diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/AnalysisResultDAO.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/AnalysisResultDAO.java index c1d04d7ca2..f3fbbc7753 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/AnalysisResultDAO.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/AnalysisResultDAO.java @@ -36,7 +36,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.logging.Level; @@ -52,6 +51,7 @@ import org.sleuthkit.autopsy.ingest.ModuleDataEvent; import org.sleuthkit.autopsy.mainui.datamodel.TreeResultsDTO.TreeDisplayCount; import org.sleuthkit.autopsy.mainui.datamodel.TreeResultsDTO.TreeItemDTO; import org.sleuthkit.autopsy.mainui.datamodel.events.DAOEventUtils; +import org.sleuthkit.autopsy.mainui.datamodel.events.KeywordHitEvent; import org.sleuthkit.autopsy.mainui.datamodel.events.TreeEvent; import org.sleuthkit.autopsy.mainui.datamodel.events.TreeCounts; import org.sleuthkit.autopsy.mainui.nodes.DAOFetcher; @@ -69,6 +69,8 @@ import org.sleuthkit.datamodel.Pool; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; +import static org.sleuthkit.datamodel.TskData.KeywordSearchQueryType.REGEX; +import static org.sleuthkit.datamodel.TskData.KeywordSearchQueryType.SUBSTRING; import org.sleuthkit.datamodel.Volume; import org.sleuthkit.datamodel.VolumeSystem; @@ -353,18 +355,6 @@ public class AnalysisResultDAO extends BlackboardArtifactDAO { return keywordHitCache.get(searchParams, () -> fetchKeywordHitsForTable(searchParams)); } - public void dropAnalysisResultCache() { - analysisResultCache.invalidateAll(); - } - - public void dropHashHitCache() { - setHitCache.invalidateAll(); - } - - public void dropKeywordHitCache() { - keywordHitCache.invalidateAll(); - } - /** * Returns a search results dto containing rows of counts data. * @@ -378,10 +368,22 @@ public class AnalysisResultDAO extends BlackboardArtifactDAO { */ public TreeResultsDTO getAnalysisResultCounts(Long dataSourceId) throws ExecutionException { try { + + Set indeterminateTypes = this.treeCounts.getEnqueued().stream() + .filter(evt -> dataSourceId == null || Objects.equals(evt.getDataSourceId(), dataSourceId)) + .map(evt -> evt.getArtifactType()) + .collect(Collectors.toSet()); + // get row dto's sorted by display name Map typeCounts = getCounts(BlackboardArtifact.Category.ANALYSIS_RESULT, dataSourceId); List> treeItemRows = typeCounts.entrySet().stream() - .map(entry -> getTreeItem(entry.getKey(), dataSourceId, TreeDisplayCount.getDeterminate(entry.getValue()))) + .map(entry -> { + TreeDisplayCount displayCount = indeterminateTypes.contains(entry.getKey()) + ? TreeDisplayCount.INDETERMINATE + : TreeDisplayCount.getDeterminate(entry.getValue()); + + return getTreeItem(entry.getKey(), dataSourceId, displayCount); + }) .sorted(Comparator.comparing(countRow -> countRow.getDisplayName())) .collect(Collectors.toList()); @@ -395,7 +397,7 @@ public class AnalysisResultDAO extends BlackboardArtifactDAO { private TreeItemDTO getTreeItem(BlackboardArtifact.Type type, Long dataSourceId, TreeDisplayCount displayCount) { return new TreeItemDTO<>( - BlackboardArtifact.Category.ANALYSIS_RESULT.name(), + AnalysisResultSearchParam.getTypeId(), new AnalysisResultSearchParam(type, dataSourceId), type.getTypeID(), type.getDisplayName(), @@ -478,16 +480,29 @@ public class AnalysisResultDAO extends BlackboardArtifactDAO { Long dataSourceId, String nullSetName) throws IllegalArgumentException, ExecutionException { + Set indeterminateSetNames = new HashSet<>(); + for (AnalysisResultEvent evt : this.treeCounts.getEnqueued()) { + if (evt instanceof AnalysisResultSetEvent + && (dataSourceId == null || Objects.equals(evt.getDataSourceId(), dataSourceId)) + && evt.getArtifactType().equals(type)) { + indeterminateSetNames.add(((AnalysisResultSetEvent) evt).getSetName()); + } + } + List> allSets = getSetCountsMap(type, BlackboardAttribute.Type.TSK_SET_NAME, dataSourceId).entrySet().stream() .filter(entry -> nullSetName != null || entry.getKey() != null) .sorted((a, b) -> compareSetStrings(a.getKey(), b.getKey())) .map(entry -> { + TreeDisplayCount displayCount = indeterminateSetNames.contains(entry.getKey()) + ? TreeDisplayCount.INDETERMINATE + : TreeDisplayCount.getDeterminate(entry.getValue()); + return getSetTreeItem(type, dataSourceId, entry.getKey(), entry.getKey() == null ? nullSetName : entry.getKey(), - TreeDisplayCount.getDeterminate(entry.getValue())); + displayCount); }) .collect(Collectors.toList()); @@ -498,7 +513,7 @@ public class AnalysisResultDAO extends BlackboardArtifactDAO { Long dataSourceId, String setName, String displayName, TreeDisplayCount displayCount) { return new TreeItemDTO<>( - type.getTypeName(), + AnalysisResultSetSearchParam.getTypeId(), new AnalysisResultSetSearchParam(type, dataSourceId, setName), setName == null ? 0 : setName, displayName, @@ -548,6 +563,18 @@ public class AnalysisResultDAO extends BlackboardArtifactDAO { throw new IllegalArgumentException("Expected data source id to be > 0"); } + Set> indeterminateSearchTerms = new HashSet<>(); + for (AnalysisResultEvent evt : this.treeCounts.getEnqueued()) { + if (evt instanceof KeywordHitEvent + && (dataSourceId == null || Objects.equals(evt.getDataSourceId(), dataSourceId)) + && evt.getArtifactType().equals(BlackboardArtifact.Type.TSK_KEYWORD_HIT) + && Objects.equals(((KeywordHitEvent) evt).getSetName(), setName)) { + + KeywordHitEvent keywordEvt = (KeywordHitEvent) evt; + indeterminateSearchTerms.add(Pair.of(keywordEvt.getSearchString(), keywordEvt.getSearchType())); + } + } + String dataSourceClause = dataSourceId == null ? "" : "AND art.data_source_obj_id = ?\n"; @@ -619,29 +646,24 @@ public class AnalysisResultDAO extends BlackboardArtifactDAO { long count = resultSet.getLong("count"); boolean hasChildren = resultSet.getBoolean("has_children"); - String searchTermModified; - switch (searchType) { - case 0: - searchTermModified = Bundle.AnalysisResultDAO_getKeywordSearchTermCounts_exactMatch(searchTerm == null ? "" : searchTerm); - break; - case 1: - searchTermModified = Bundle.AnalysisResultDAO_getKeywordSearchTermCounts_substringMatch(searchTerm == null ? "" : searchTerm); - break; - case 2: - searchTermModified = Bundle.AnalysisResultDAO_getKeywordSearchTermCounts_regexMatch(searchTerm == null ? "" : searchTerm); - break; - default: - logger.log(Level.WARNING, MessageFormat.format("Non-standard search type value: {0}.", searchType)); - searchTermModified = searchTerm; - break; - } + TskData.KeywordSearchQueryType searchTypeEnum = + Stream.of(TskData.KeywordSearchQueryType.values()) + .filter(tp -> tp.getType() == searchType) + .findFirst() + .orElse(TskData.KeywordSearchQueryType.LITERAL); + + String searchTermModified = getSearchTermDisplayName(searchTerm, searchTypeEnum); + + TreeDisplayCount displayCount = indeterminateSearchTerms.contains(Pair.of(searchTerm, searchType)) + ? TreeDisplayCount.INDETERMINATE + : TreeDisplayCount.getDeterminate(count); TreeItemDTO treeItem = new TreeItemDTO<>( - "KEYWORD_SEARCH_TERMS", + KeywordSearchTermParams.getTypeId(), new KeywordSearchTermParams(setName, searchTerm, TskData.KeywordSearchQueryType.valueOf(searchType), hasChildren, dataSourceId), searchTermModified, searchTermModified, - TreeDisplayCount.getDeterminate(count) + displayCount ); items.add(treeItem); @@ -658,6 +680,34 @@ public class AnalysisResultDAO extends BlackboardArtifactDAO { } } + /** + * Returns the UI display name for a search term. + * + * @param searchTerm The search term. + * @param searchType The search type enum value. + * + * @return The display name. + */ + public String getSearchTermDisplayName(String searchTerm, TskData.KeywordSearchQueryType searchType) { + String searchTermModified; + switch (searchType) { + case LITERAL: + searchTermModified = Bundle.AnalysisResultDAO_getKeywordSearchTermCounts_exactMatch(searchTerm == null ? "" : searchTerm); + break; + case SUBSTRING: + searchTermModified = Bundle.AnalysisResultDAO_getKeywordSearchTermCounts_substringMatch(searchTerm == null ? "" : searchTerm); + break; + case REGEX: + searchTermModified = Bundle.AnalysisResultDAO_getKeywordSearchTermCounts_regexMatch(searchTerm == null ? "" : searchTerm); + break; + default: + logger.log(Level.WARNING, MessageFormat.format("Non-standard search type value: {0}.", searchType)); + searchTermModified = searchTerm; + break; + } + return searchTermModified; + } + /** * Get counts for string matches of a particular regex/substring search * term. @@ -707,6 +757,23 @@ public class AnalysisResultDAO extends BlackboardArtifactDAO { + "AND res.search_type = ?\n" + "GROUP BY keyword"; + Set indeterminateMatches = new HashSet<>(); + for (AnalysisResultEvent evt : this.treeCounts.getEnqueued()) { + if (evt instanceof KeywordHitEvent + && (dataSourceId == null || Objects.equals(evt.getDataSourceId(), dataSourceId)) + && evt.getArtifactType().equals(BlackboardArtifact.Type.TSK_KEYWORD_HIT)) { + + KeywordHitEvent keywordEvt = (KeywordHitEvent) evt; + if (Objects.equals(keywordEvt.getSetName(), setName) + && Objects.equals(keywordEvt.getSearchString(), regexStr) + && keywordEvt.getSearchType() == searchType) { + + indeterminateMatches.add(keywordEvt.getMatch()); + } + + } + } + try (CaseDbPreparedStatement preparedStatement = getCase().getCaseDbAccessManager().prepareSelect(query)) { // get artifact types and counts int paramIdx = 0; @@ -728,12 +795,17 @@ public class AnalysisResultDAO extends BlackboardArtifactDAO { String keyword = resultSet.getString("keyword"); long count = resultSet.getLong("count"); + TreeDisplayCount displayCount = indeterminateMatches.contains(keyword) + ? TreeDisplayCount.INDETERMINATE + : TreeDisplayCount.getDeterminate(count); + items.add(new TreeItemDTO<>( - "KEYWORD_MATCH", + KeywordMatchParams.getTypeId(), new KeywordMatchParams(setName, regexStr, keyword, searchType, dataSourceId), keyword, keyword == null ? "" : keyword, - TreeDisplayCount.getDeterminate(count))); + displayCount + )); } } catch (SQLException ex) { logger.log(Level.WARNING, "An error occurred while fetching results from result set.", ex); @@ -755,7 +827,7 @@ public class AnalysisResultDAO extends BlackboardArtifactDAO { } @Override - Set processEvent(PropertyChangeEvent evt) { + Set processEvent(PropertyChangeEvent evt) { // get a grouping of artifacts mapping the artifact type id to data source id. Map> analysisResultMap = new HashMap<>(); Map, Set> setMap = new HashMap<>(); @@ -788,16 +860,11 @@ public class AnalysisResultDAO extends BlackboardArtifactDAO { return Collections.emptySet(); } - clearRelevantCacheEntries(analysisResultMap, setMap); + SubDAOUtils.invalidateKeys(this.analysisResultCache, ar -> Pair.of(ar.getArtifactType(), ar.getDataSourceId()), analysisResultMap); + SubDAOUtils.invalidateKeys(this.setHitCache, ar -> Pair.of(Pair.of(ar.getArtifactType(), ar.getSetName()), ar.getDataSourceId()), setMap); - List daoEvents = getResultViewEvents(analysisResultMap, setMap); - Collection treeEvents = this.treeCounts.enqueueAll(daoEvents).stream() - .map(arEvt -> getTreeEvent(arEvt, false)) - .collect(Collectors.toList()); - - return Stream.of(daoEvents, treeEvents) - .flatMap(lst -> lst.stream()) - .collect(Collectors.toSet()); + // GVDTODO handle keyword hits + return getResultViewEvents(analysisResultMap, setMap); } /** @@ -813,7 +880,7 @@ public class AnalysisResultDAO extends BlackboardArtifactDAO { * * @return The list of dao events. */ - private List getResultViewEvents(Map> analysisResultMap, Map, Set> resultsWithSetMap) { + private Set getResultViewEvents(Map> analysisResultMap, Map, Set> resultsWithSetMap) { Stream analysisResultEvts = analysisResultMap.entrySet().stream() .flatMap(entry -> entry.getValue().stream().map(dsId -> new AnalysisResultEvent(entry.getKey(), dsId))); @@ -821,82 +888,50 @@ public class AnalysisResultDAO extends BlackboardArtifactDAO { .flatMap(entry -> entry.getValue().stream().map(dsId -> new AnalysisResultSetEvent(entry.getKey().getRight(), entry.getKey().getLeft(), dsId))); // GVDTODO handle keyword hits - return Stream.of(analysisResultEvts, analysisResultSetEvts) + + List daoEvents = Stream.of(analysisResultEvts, analysisResultSetEvts) .flatMap(s -> s) .collect(Collectors.toList()); + + Collection treeEvents = this.treeCounts.enqueueAll(daoEvents).stream() + .map(arEvt -> new TreeEvent(getTreeItem(arEvt, TreeDisplayCount.INDETERMINATE), false)) + .collect(Collectors.toList()); + + return Stream.of(daoEvents, treeEvents) + .flatMap(lst -> lst.stream()) + .collect(Collectors.toSet()); } /** - * Clears cache entries given the provided digests of autopsy events. - * - * @param analysisResultMap Contains the analysis results that do not use a - * set name. A mapping of analysis result type ids - * to data sources where the results were created. - * @param resultsWithSetMap Contains the anlaysis results that do use a set - * name. A mapping of (analysis result type id, set - * name) to data sources where results were - * created. - */ - private void clearRelevantCacheEntries(Map> analysisResultMap, Map, Set> resultsWithSetMap) { - ConcurrentMap, AnalysisResultTableSearchResultsDTO> arConcurrentMap = this.analysisResultCache.asMap(); - arConcurrentMap.forEach((k, v) -> { - BlackboardArtifactSearchParam searchParam = k.getParamData(); - Set dsIds = analysisResultMap.get(searchParam.getArtifactType()); - if (dsIds != null && (searchParam.getDataSourceId() == null || dsIds.contains(searchParam.getDataSourceId()))) { - arConcurrentMap.remove(k); - } - }); - - ConcurrentMap, AnalysisResultTableSearchResultsDTO> setConcurrentMap = this.setHitCache.asMap(); - setConcurrentMap.forEach((k, v) -> { - AnalysisResultSetSearchParam searchParam = k.getParamData(); - Set dsIds = resultsWithSetMap.get(Pair.of(searchParam.getArtifactType(), searchParam.getSetName())); - if (dsIds != null && (searchParam.getDataSourceId() == null || dsIds.contains(searchParam.getDataSourceId()))) { - arConcurrentMap.remove(k); - } - }); - - // GVDTODO handle clearing cache for keyword search hits - // private final Cache, AnalysisResultTableSearchResultsDTO> keywordHitCache = CacheBuilder.newBuilder().maximumSize(1000).build(); - } - - /** - * Creates a TreeEvent instance based on the analysis result event and + * Creates a TreeItemDTO instance based on the analysis result event and * whether or not this event should trigger a full refresh of counts. * - * @param arEvt The analysis result event. - * @param shouldRefresh Whether or not this tree event should trigger a full - * refresh of counts. + * @param arEvt The analysis result event. + * @param displayCount The count to display. * * @return The tree event. */ - private TreeEvent getTreeEvent(AnalysisResultEvent arEvt, boolean shouldRefresh) { + private TreeItemDTO getTreeItem(AnalysisResultEvent arEvt, TreeDisplayCount displayCount) { // GVDTODO handle keyword items when integrated if (arEvt instanceof AnalysisResultSetEvent) { AnalysisResultSetEvent setEvt = (AnalysisResultSetEvent) arEvt; - return new TreeEvent(getSetTreeItem(setEvt.getArtifactType(), setEvt.getDataSourceId(), + return getSetTreeItem(setEvt.getArtifactType(), setEvt.getDataSourceId(), setEvt.getSetName(), setEvt.getSetName() == null ? "" : setEvt.getSetName(), - shouldRefresh ? TreeDisplayCount.UNSPECIFIED : TreeDisplayCount.INDETERMINATE), - shouldRefresh); + displayCount); } else { - return new TreeEvent(getTreeItem(arEvt.getArtifactType(), arEvt.getDataSourceId(), - shouldRefresh ? TreeDisplayCount.UNSPECIFIED : TreeDisplayCount.INDETERMINATE), - shouldRefresh); + return getTreeItem(arEvt.getArtifactType(), arEvt.getDataSourceId(), displayCount); } } @Override - Set handleIngestComplete() { - return this.treeCounts.flushEvents().stream() - .map(arEvt -> getTreeEvent(arEvt, true)) - .collect(Collectors.toSet()); + Set handleIngestComplete() { + return SubDAOUtils.getIngestCompleteEvents(this.treeCounts, (arEvt, count) -> getTreeItem(arEvt, count)); } @Override Set shouldRefreshTree() { - return this.treeCounts.getEventTimeouts().stream() - .map(arEvt -> getTreeEvent(arEvt, true)) - .collect(Collectors.toSet()); + return SubDAOUtils.getRefreshEvents(this.treeCounts, (arEvt, count) -> getTreeItem(arEvt, count)); + } /** diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/AnalysisResultSearchParam.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/AnalysisResultSearchParam.java index 4f2805a650..c9ad697a82 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/AnalysisResultSearchParam.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/AnalysisResultSearchParam.java @@ -24,8 +24,17 @@ import org.sleuthkit.datamodel.BlackboardArtifact; * Key for analysis result in order to retrieve data from DAO. */ public class AnalysisResultSearchParam extends BlackboardArtifactSearchParam { - + + private static final String TYPE_ID = BlackboardArtifact.Category.ANALYSIS_RESULT.name(); + + /** + * @return The type id for this search parameter. + */ + public static String getTypeId() { + return TYPE_ID; + } + public AnalysisResultSearchParam(BlackboardArtifact.Type artifactType, Long dataSourceId) { super(artifactType, dataSourceId); - } + } } diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/AnalysisResultSetSearchParam.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/AnalysisResultSetSearchParam.java index 767491c3fe..ca4480d708 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/AnalysisResultSetSearchParam.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/AnalysisResultSetSearchParam.java @@ -25,14 +25,23 @@ import org.sleuthkit.datamodel.BlackboardArtifact; * Base class for search params for analysis results that filter by set name. */ public class AnalysisResultSetSearchParam extends AnalysisResultSearchParam { - + + private static final String TYPE_ID = "ANALYSIS_RESULT_SET"; + + /** + * @return The type id for this search parameter. + */ + public static String getTypeId() { + return TYPE_ID; + } + private final String setName; public AnalysisResultSetSearchParam(BlackboardArtifact.Type artifactType, Long dataSourceId, String setName) { super(artifactType, dataSourceId); this.setName = setName; } - + public String getSetName() { return setName; } @@ -63,5 +72,4 @@ public class AnalysisResultSetSearchParam extends AnalysisResultSearchParam { return super.equals(obj); } - } diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/BlackboardArtifactSearchParam.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/BlackboardArtifactSearchParam.java index 2bfd9a3929..8fd1f38c1d 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/BlackboardArtifactSearchParam.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/BlackboardArtifactSearchParam.java @@ -25,6 +25,16 @@ import org.sleuthkit.datamodel.BlackboardArtifact; * Key for data artifact in order to retrieve data from DAO. */ public class BlackboardArtifactSearchParam { + + private static final String TYPE_ID = "BLACKBOARD_ARTIFACT"; + + /** + * @return The type id for this search parameter. + */ + public static String getTypeId() { + return TYPE_ID; + } + private final BlackboardArtifact.Type artifactType; private final Long dataSourceId; diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/CommAccountsDAO.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/CommAccountsDAO.java index 186817ab24..c9a039153d 100755 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/CommAccountsDAO.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/CommAccountsDAO.java @@ -160,7 +160,7 @@ public class CommAccountsDAO extends AbstractDAO { private static TreeResultsDTO.TreeItemDTO createAccountTreeItem(Account.Type accountType, Long dataSourceId, TreeResultsDTO.TreeDisplayCount count) { return new TreeResultsDTO.TreeItemDTO<>( - "ACCOUNTS", + CommAccountsSearchParams.getTypeId(), new CommAccountsSearchParams(accountType, dataSourceId), accountType.getTypeName(), accountType.getDisplayName(), @@ -236,7 +236,7 @@ public class CommAccountsDAO extends AbstractDAO { Set handleIngestComplete() { return SubDAOUtils.getIngestCompleteEvents( this.accountCounts, - (daoEvt) -> createAccountTreeItem(daoEvt.getAccountType(), daoEvt.getDataSourceId(), TreeResultsDTO.TreeDisplayCount.UNSPECIFIED) + (daoEvt, count) -> createAccountTreeItem(daoEvt.getAccountType(), daoEvt.getDataSourceId(), count) ); } @@ -244,7 +244,7 @@ public class CommAccountsDAO extends AbstractDAO { Set shouldRefreshTree() { return SubDAOUtils.getRefreshEvents( this.accountCounts, - (daoEvt) -> createAccountTreeItem(daoEvt.getAccountType(), daoEvt.getDataSourceId(), TreeResultsDTO.TreeDisplayCount.UNSPECIFIED) + (daoEvt, count) -> createAccountTreeItem(daoEvt.getAccountType(), daoEvt.getDataSourceId(), count) ); } diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/CommAccountsSearchParams.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/CommAccountsSearchParams.java index 8b187b15a7..d20b427168 100755 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/CommAccountsSearchParams.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/CommAccountsSearchParams.java @@ -27,9 +27,18 @@ import org.sleuthkit.datamodel.BlackboardArtifact; */ public class CommAccountsSearchParams extends DataArtifactSearchParam { + private static final String TYPE_ID = "DATA_ARTIFACT_ACCOUNT"; + + /** + * @return The type id for this search parameter. + */ + public static String getTypeId() { + return TYPE_ID; + } + private final Account.Type type; private final Long dataSourceId; - + public CommAccountsSearchParams(Account.Type type, Long dataSourceId) { super(BlackboardArtifact.Type.TSK_ACCOUNT, dataSourceId); this.type = type; diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/DataArtifactDAO.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/DataArtifactDAO.java index 4728c27010..3c152fc152 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/DataArtifactDAO.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/DataArtifactDAO.java @@ -245,7 +245,7 @@ public class DataArtifactDAO extends BlackboardArtifactDAO { private TreeItemDTO createDataArtifactTreeItem(BlackboardArtifact.Type artifactType, Long dataSourceId, TreeDisplayCount displayCount) { return new TreeResultsDTO.TreeItemDTO<>( - BlackboardArtifact.Category.DATA_ARTIFACT.name(), + DataArtifactSearchParam.getTypeId(), new DataArtifactSearchParam(artifactType, dataSourceId), artifactType.getTypeID(), getDisplayName(artifactType), @@ -255,13 +255,13 @@ public class DataArtifactDAO extends BlackboardArtifactDAO { @Override Set handleIngestComplete() { return SubDAOUtils.getIngestCompleteEvents(this.treeCounts, - (daoEvt) -> createDataArtifactTreeItem(daoEvt.getArtifactType(), daoEvt.getDataSourceId(), TreeDisplayCount.UNSPECIFIED)); + (daoEvt, count) -> createDataArtifactTreeItem(daoEvt.getArtifactType(), daoEvt.getDataSourceId(), count)); } @Override Set shouldRefreshTree() { return SubDAOUtils.getRefreshEvents(this.treeCounts, - (daoEvt) -> createDataArtifactTreeItem(daoEvt.getArtifactType(), daoEvt.getDataSourceId(), TreeDisplayCount.UNSPECIFIED)); + (daoEvt, count) -> createDataArtifactTreeItem(daoEvt.getArtifactType(), daoEvt.getDataSourceId(), count)); } diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/DataArtifactSearchParam.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/DataArtifactSearchParam.java index 0956cf4b5c..9fff59cd28 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/DataArtifactSearchParam.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/DataArtifactSearchParam.java @@ -25,7 +25,16 @@ import org.sleuthkit.datamodel.BlackboardArtifact; */ public class DataArtifactSearchParam extends BlackboardArtifactSearchParam { + private static final String TYPE_ID = BlackboardArtifact.Category.DATA_ARTIFACT.name(); + + /** + * @return The type id for this search parameter. + */ + public static String getTypeId() { + return TYPE_ID; + } + public DataArtifactSearchParam(BlackboardArtifact.Type artifactType, Long dataSourceId) { - super (artifactType, dataSourceId); - } + super(artifactType, dataSourceId); + } } diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileSystemContentSearchParam.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileSystemContentSearchParam.java index 71260b8aaa..4740f2d482 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileSystemContentSearchParam.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileSystemContentSearchParam.java @@ -24,7 +24,17 @@ import org.sleuthkit.autopsy.mainui.nodes.NodeSelectionInfo.ContentNodeSelection /** * Key for content object in order to retrieve data from DAO. */ -public class FileSystemContentSearchParam implements ContentNodeSelectionInfo{ +public class FileSystemContentSearchParam implements ContentNodeSelectionInfo { + + private static final String TYPE_ID = "FILE_SYSTEM_CONTENT"; + + /** + * @return The type id for this search parameter. + */ + public static String getTypeId() { + return TYPE_ID; + } + private final Long contentObjectId; // This param is can change, is not used as part of the search query and diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileSystemDAO.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileSystemDAO.java index ce504c21a0..7c06adcbc8 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileSystemDAO.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileSystemDAO.java @@ -23,11 +23,10 @@ import com.google.common.cache.CacheBuilder; import com.google.common.collect.ImmutableSet; import java.beans.PropertyChangeEvent; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.Comparator; -import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentMap; @@ -58,11 +57,14 @@ import org.sleuthkit.autopsy.mainui.datamodel.FileRowDTO.LayoutFileRowDTO; import org.sleuthkit.autopsy.mainui.datamodel.FileRowDTO.SlackFileRowDTO; import org.sleuthkit.autopsy.mainui.datamodel.ContentRowDTO.PoolRowDTO; import org.sleuthkit.autopsy.mainui.datamodel.TreeResultsDTO.TreeDisplayCount; +import org.sleuthkit.autopsy.mainui.datamodel.TreeResultsDTO.TreeItemDTO; import org.sleuthkit.autopsy.mainui.datamodel.events.FileSystemContentEvent; import org.sleuthkit.autopsy.mainui.datamodel.events.FileSystemHostEvent; import org.sleuthkit.autopsy.mainui.datamodel.events.FileSystemPersonEvent; +import org.sleuthkit.autopsy.mainui.datamodel.events.TreeCounts; import org.sleuthkit.autopsy.mainui.datamodel.events.TreeEvent; import org.sleuthkit.autopsy.mainui.nodes.DAOFetcher; +import org.sleuthkit.datamodel.AbstractContent; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.DataSource; @@ -110,8 +112,11 @@ public class FileSystemDAO extends AbstractDAO { Case.Events.HOSTS_REMOVED_FROM_PERSON.toString() ); - private final Cache, BaseSearchResultsDTO> searchParamsCache = CacheBuilder.newBuilder().maximumSize(CACHE_SIZE).expireAfterAccess(CACHE_DURATION, CACHE_DURATION_UNITS).build(); - + private final Cache, BaseSearchResultsDTO> searchParamsCache + = CacheBuilder.newBuilder().maximumSize(CACHE_SIZE).expireAfterAccess(CACHE_DURATION, CACHE_DURATION_UNITS).build(); + + private final TreeCounts treeCounts = new TreeCounts<>(); + private static final String FILE_SYSTEM_TYPE_ID = "FILE_SYSTEM"; private static FileSystemDAO instance = null; @@ -130,7 +135,7 @@ public class FileSystemDAO extends AbstractDAO { FileSystemContentEvent contentEvt = (FileSystemContentEvent) daoEvent; - return contentEvt.getContentObjectId() == null || key.getContentObjectId().equals(contentEvt.getContentObjectId()); + return contentEvt.getContentObjectId() == null || Objects.equals(key.getContentObjectId(), contentEvt.getContentObjectId()); } private boolean isSystemHostInvalidating(FileSystemHostSearchParam key, DAOEvent daoEvent) { @@ -315,19 +320,13 @@ public class FileSystemDAO extends AbstractDAO { return searchParamsCache.get(searchParams, () -> fetchHostsForTable(searchParams)); } - @Override - void clearCaches() { - this.searchParamsCache.invalidateAll(); - } - - private Long getHostFromDs(Content dataSource) { + private Host getHostFromDs(Content dataSource) { if (!(dataSource instanceof DataSource)) { return null; } try { - Host host = ((DataSource) dataSource).getHost(); - return host == null ? null : host.getHostId(); + return ((DataSource) dataSource).getHost(); } catch (TskCoreException ex) { logger.log(Level.WARNING, "There was an error getting the host for data source with id: " + dataSource.getId(), ex); return null; @@ -358,24 +357,16 @@ public class FileSystemDAO extends AbstractDAO { return false; } - @Override - Set handleIngestComplete() { - // GVDTODO - return Collections.emptySet(); - } - - @Override - Set shouldRefreshTree() { - // GVDTODO - return Collections.emptySet(); - } - @Override Set processEvent(PropertyChangeEvent evt) { - // GVDTODO these can probably be rewritten now that it isn't handling a collection of autopsy events - Set affectedPersons = new HashSet<>(); - Set affectedHosts = new HashSet<>(); - Set affectedParentContent = new HashSet<>(); + Content affectedContent = null; + Content affectedParentContent = null; + Host affectedParentHost = null; + + // GVDTODO person parents and parent of persons not handled yet + // optional present but null indicates no person parent + Optional affectedParentPerson = Optional.empty(); + boolean refreshAllContent = false; Content content = DAOEventUtils.getDerivedFileContentFromFileEvent(evt); @@ -388,132 +379,158 @@ public class FileSystemDAO extends AbstractDAO { return Collections.emptySet(); } - if (parentContent == null) { - return Collections.emptySet(); - } - if (invalidatesAllFileSystem(parentContent)) { refreshAllContent = true; } else { - affectedParentContent.add(parentContent.getId()); + affectedContent = content; + affectedParentContent = parentContent; } } else if (evt instanceof DataSourceAddedEvent) { - Long hostId = getHostFromDs(((DataSourceAddedEvent) evt).getDataSource()); - if (hostId != null) { - affectedHosts.add(hostId); - } + Host host = getHostFromDs(((DataSourceAddedEvent) evt).getDataSource()); + affectedParentHost = host; + } else if (evt instanceof DataSourceNameChangedEvent) { - Long hostId = getHostFromDs(((DataSourceNameChangedEvent) evt).getDataSource()); - if (hostId != null) { - affectedHosts.add(hostId); - } + Host host = getHostFromDs(((DataSourceNameChangedEvent) evt).getDataSource()); + affectedParentHost = host; + } else if (evt instanceof HostsAddedEvent) { // GVDTODO how best to handle host added? } else if (evt instanceof HostsUpdatedEvent) { // GVDTODO how best to handle host updated? } else if (evt instanceof HostsAddedToPersonEvent) { Person person = ((HostsAddedToPersonEvent) evt).getPerson(); - affectedPersons.add(person == null ? null : person.getPersonId()); + affectedParentPerson = Optional.of(person); } else if (evt instanceof HostsRemovedFromPersonEvent) { Person person = ((HostsRemovedFromPersonEvent) evt).getPerson(); - affectedPersons.add(person == null ? null : person.getPersonId()); + affectedParentPerson = Optional.of(person); } - final boolean triggerFullRefresh = refreshAllContent; + // if nothing affected, return no events + if (!refreshAllContent && affectedContent == null && affectedParentHost == null && !affectedParentPerson.isPresent()) { + return Collections.emptySet(); + } - // GVDTODO handling null ids versus the 'No Persons' option - ConcurrentMap, BaseSearchResultsDTO> concurrentMap = this.searchParamsCache.asMap(); - concurrentMap.forEach((k, v) -> { - Object searchParams = k.getParamData(); - if (searchParams instanceof FileSystemPersonSearchParam) { - FileSystemPersonSearchParam personParam = (FileSystemPersonSearchParam) searchParams; - if (affectedPersons.contains(personParam.getPersonObjectId())) { - concurrentMap.remove(k); - } - } else if (searchParams instanceof FileSystemHostSearchParam) { - FileSystemHostSearchParam hostParams = (FileSystemHostSearchParam) searchParams; - if (affectedHosts.contains(hostParams.getHostObjectId())) { - concurrentMap.remove(k); - } - } else if (searchParams instanceof FileSystemContentSearchParam) { - FileSystemContentSearchParam contentParams = (FileSystemContentSearchParam) searchParams; - if (triggerFullRefresh - || contentParams.getContentObjectId() == null - || affectedParentContent.contains(contentParams.getContentObjectId())) { - concurrentMap.remove(k); - } + invalidateKeys(affectedParentPerson, affectedParentHost, affectedContent, refreshAllContent); + + return getDAOEvents(affectedParentPerson, affectedParentHost, affectedContent, affectedParentContent, refreshAllContent); + } + + private Set getDAOEvents(Optional affectedPerson, Host affectedHost, Content affectedContent, Content affectedParentContent, boolean triggerFullRefresh) { + List daoEvents = new ArrayList<>(); + + if (triggerFullRefresh) { + daoEvents.add(new FileSystemContentEvent(null, null, null)); + } else if (affectedContent != null) { + Host parentHost = null; + + try { + parentHost = (affectedContent instanceof DataSource) + ? ((DataSource) affectedContent).getHost() + : null; + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "An error occurred while fetching content id and host id for content with id of: " + affectedContent.getId(), ex); } + + daoEvents.add(new FileSystemContentEvent(affectedContent, affectedParentContent == null ? null : affectedParentContent.getId(), parentHost)); + } + + if (affectedHost != null) { + daoEvents.add(new FileSystemHostEvent(affectedHost.getHostId())); + } + + affectedPerson.ifPresent((person) -> { + daoEvents.add(new FileSystemPersonEvent(person == null ? null : person.getPersonId())); }); - Stream fileEvts = triggerFullRefresh - ? Stream.of(new FileSystemContentEvent(null)) - : affectedParentContent.stream().map(id -> new FileSystemContentEvent(id)); + List treeEvents = this.treeCounts.enqueueAll(daoEvents).stream() + .map(daoEvt -> createTreeEvent(daoEvt, TreeDisplayCount.INDETERMINATE, false)) + .filter(evt -> evt != null) + .collect(Collectors.toList()); - return Stream.of( - affectedPersons.stream().map(id -> new FileSystemPersonEvent(id)), - affectedHosts.stream().map(id -> new FileSystemHostEvent(id)), - fileEvts - ) - .flatMap(s -> s) + return Stream.of(daoEvents, treeEvents) + .flatMap(lst -> lst.stream()) .collect(Collectors.toSet()); } - + + private void invalidateKeys(Optional affectedPerson, Host affectedHost, Content affectedContent, boolean triggerFullRefresh) { + ConcurrentMap, ?> concurrentMap = this.searchParamsCache.asMap(); + concurrentMap.forEach((k, v) -> { + Object searchParams = k.getParamData(); + boolean shouldInvalidate = false; + if (searchParams instanceof FileSystemPersonSearchParam && affectedPerson.isPresent()) { + shouldInvalidate = Objects.equals( + ((FileSystemPersonSearchParam) searchParams).getPersonObjectId(), + // to allow for null parent person + affectedPerson.flatMap(p -> Optional.ofNullable(p.getPersonId())).orElse(null) + ); + + } else if (searchParams instanceof FileSystemHostSearchParam && affectedHost != null) { + shouldInvalidate = Objects.equals( + ((FileSystemHostSearchParam) searchParams).getHostObjectId(), + affectedHost.getHostId() + ); + + } else if (searchParams instanceof FileSystemContentSearchParam) { + if (triggerFullRefresh) { + shouldInvalidate = true; + } else if (affectedContent != null) { + shouldInvalidate = Objects.equals( + ((FileSystemContentSearchParam) searchParams).getContentObjectId(), + affectedContent.getId() + ); + } + } + + if (shouldInvalidate) { + concurrentMap.remove(k); + } + }); + } + /** * Get all data sources belonging to a given host. - * + * * @param host The host. - * + * * @return Results containing all data sources for the given host. - * - * @throws ExecutionException + * + * @throws ExecutionException */ public TreeResultsDTO getDataSourcesForHost(Host host) throws ExecutionException { try { List> treeItemRows = new ArrayList<>(); for (DataSource ds : Case.getCurrentCaseThrows().getSleuthkitCase().getHostManager().getDataSourcesForHost(host)) { - treeItemRows.add(new TreeResultsDTO.TreeItemDTO<>( - ds.getClass().getSimpleName(), - new FileSystemContentSearchParam(ds.getId()), - ds, - ds.getName(), - null - )); + treeItemRows.add(createDisplayableContentTreeItem(ds, TreeDisplayCount.NOT_SHOWN)); } return new TreeResultsDTO<>(treeItemRows); } catch (NoCurrentCaseException | TskCoreException ex) { throw new ExecutionException("An error occurred while fetching images for host with ID " + host.getHostId(), ex); } } - + /** * Create results for a single given data source ID (not its children). - * + * * @param dataSourceObjId The data source object ID. - * + * * @return Results containing just this data source. - * - * @throws ExecutionException + * + * @throws ExecutionException */ public TreeResultsDTO getSingleDataSource(long dataSourceObjId) throws ExecutionException { try { List> treeItemRows = new ArrayList<>(); DataSource ds = Case.getCurrentCaseThrows().getSleuthkitCase().getDataSource(dataSourceObjId); - treeItemRows.add(new TreeResultsDTO.TreeItemDTO<>( - ds.getClass().getSimpleName(), - new FileSystemContentSearchParam(ds.getId()), - ds, - ds.getName(), - null - )); - + treeItemRows.add(createDisplayableContentTreeItem(ds, TreeDisplayCount.NOT_SHOWN)); return new TreeResultsDTO<>(treeItemRows); } catch (NoCurrentCaseException | TskCoreException | TskDataException ex) { throw new ExecutionException("An error occurred while fetching data source with ID " + dataSourceObjId, ex); } } - + /** - * Get the children that will be displayed in the tree for a given content ID. + * Get the children that will be displayed in the tree for a given content + * ID. * * @param contentId Object ID of parent content. * @@ -523,23 +540,18 @@ public class FileSystemDAO extends AbstractDAO { */ public TreeResultsDTO getDisplayableContentChildren(Long contentId) throws ExecutionException { try { - + List treeChildren = FileSystemColumnUtils.getVisibleTreeNodeChildren(contentId); - + List> treeItemRows = new ArrayList<>(); for (Content child : treeChildren) { Long countForNode = null; if ((child instanceof AbstractFile) - && ! (child instanceof LocalFilesDataSource)) { + && !(child instanceof LocalFilesDataSource)) { countForNode = getContentForTable(new FileSystemContentSearchParam(child.getId()), 0, null).getTotalResultsCount(); } - treeItemRows.add(new TreeResultsDTO.TreeItemDTO<>( - child.getClass().getSimpleName(), - new FileSystemContentSearchParam(child.getId()), - child, - getNameForContent(child), - countForNode == null ? TreeDisplayCount.NOT_SHOWN : TreeDisplayCount.getDeterminate(countForNode) - )); + TreeDisplayCount displayCount = countForNode == null ? TreeDisplayCount.NOT_SHOWN : TreeDisplayCount.getDeterminate(countForNode); + treeItemRows.add(createDisplayableContentTreeItem(child, displayCount)); } return new TreeResultsDTO<>(treeItemRows); @@ -547,12 +559,23 @@ public class FileSystemDAO extends AbstractDAO { throw new ExecutionException("An error occurred while fetching data artifact counts.", ex); } } - + + private FileSystemTreeItem createDisplayableContentTreeItem(Content child, TreeDisplayCount displayCount) { + return new FileSystemTreeItem( + FileSystemContentSearchParam.getTypeId(), + new FileSystemContentSearchParam(child == null ? null : child.getId()), + child, + child == null ? null : getNameForContent(child), + child instanceof AbstractFile ? ((AbstractFile) child).getMetaType() : null, + displayCount + ); + } + /** * Get display name for the given content. - * + * * @param content The content. - * + * * @return Display name for the content. */ private String getNameForContent(Content content) { @@ -562,6 +585,132 @@ public class FileSystemDAO extends AbstractDAO { return content.getName(); } + private TreeEvent createTreeEvent(DAOEvent daoEvent, TreeDisplayCount count, boolean fullRefresh) { + + if (daoEvent instanceof FileSystemContentEvent) { + FileSystemContentEvent contentEvt = (FileSystemContentEvent) daoEvent; + + return new FileSystemTreeEvent( + contentEvt.getParentObjId(), + contentEvt.getParentHost(), + createDisplayableContentTreeItem(contentEvt.getContent(), count), + fullRefresh); + + } else if (daoEvent instanceof FileSystemHostEvent) { + // GVDTODO not currently integrated into tree + } else if (daoEvent instanceof FileSystemPersonEvent) { + // GVDTODO not currently integrated into tree + } + + return null; + } + + @Override + void clearCaches() { + this.searchParamsCache.invalidateAll(); + handleIngestComplete(); + } + + @Override + Set handleIngestComplete() { + return treeCounts.flushEvents().stream() + .map(daoEvt -> createTreeEvent(daoEvt, TreeDisplayCount.UNSPECIFIED, true)) + .filter(evt -> evt != null) + .collect(Collectors.toSet()); + } + + @Override + Set shouldRefreshTree() { + return treeCounts.getEventTimeouts().stream() + .map(daoEvt -> createTreeEvent(daoEvt, TreeDisplayCount.UNSPECIFIED, true)) + .filter(evt -> evt != null) + .collect(Collectors.toSet()); + } + + public static class DataSourceRefreshTreeEvent extends TreeEvent { + + public DataSourceRefreshTreeEvent(boolean refresh) { + super(null, refresh); + } + + } + + public static class FileSystemTreeItem extends TreeItemDTO { + + private final TskData.TSK_FS_META_TYPE_ENUM metaType; + + FileSystemTreeItem( + String typeId, + FileSystemContentSearchParam searchParams, + Object id, + String displayName, + TskData.TSK_FS_META_TYPE_ENUM metaType, + TreeDisplayCount count) { + + super(typeId, searchParams, id, displayName, count); + this.metaType = metaType; + } + + public TskData.TSK_FS_META_TYPE_ENUM getMetaType() { + return metaType; + } + + } + + public static class FileSystemTreeEvent extends TreeEvent { + + private final Long parentContentId; + private final Host parentHost; + private final FileSystemTreeItem itemRecord; + + FileSystemTreeEvent(Long parentContentId, Host parentHost, FileSystemTreeItem itemRecord, boolean refreshRequired) { + super(itemRecord, refreshRequired); + this.parentContentId = parentContentId; + this.parentHost = parentHost; + this.itemRecord = itemRecord; + } + + public Long getParentContentId() { + return parentContentId; + } + + public Host getParentHost() { + return parentHost; + } + + @Override + public FileSystemTreeItem getItemRecord() { + // override to be typed and contain extra information + return itemRecord; + } + + @Override + public int hashCode() { + int hash = 5; + hash = 31 * hash + Objects.hashCode(this.itemRecord); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final FileSystemTreeEvent other = (FileSystemTreeEvent) obj; + if (!Objects.equals(this.itemRecord, other.itemRecord)) { + return false; + } + return true; + } + + } + /** * Handles fetching and paging of data for file types by mime type. */ diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileSystemHostSearchParam.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileSystemHostSearchParam.java index 2f78e597a8..a258b2320d 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileSystemHostSearchParam.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileSystemHostSearchParam.java @@ -24,6 +24,16 @@ import java.util.Objects; * Key for content object in order to retrieve data from DAO. */ public class FileSystemHostSearchParam { + + private static final String TYPE_ID = "FILE_SYSTEM_HOST"; + + /** + * @return The type id for this search parameter. + */ + public static String getTypeId() { + return TYPE_ID; + } + private final Long hostObjectId; public FileSystemHostSearchParam(Long hostObjectId) { @@ -59,4 +69,3 @@ public class FileSystemHostSearchParam { return true; } } - diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileSystemPersonSearchParam.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileSystemPersonSearchParam.java index 99f75b1764..ae198a89dd 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileSystemPersonSearchParam.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileSystemPersonSearchParam.java @@ -24,12 +24,23 @@ import java.util.Objects; * Key for person object in order to retrieve data from DAO. */ public class FileSystemPersonSearchParam { + + private static final String TYPE_ID = "FILE_SYSTEM_PERSON"; + + /** + * @return The type id for this search parameter. + */ + public static String getTypeId() { + return TYPE_ID; + } + private final Long personObjectId; /** * Create search param. - * - * @param personObjectId May be null to fetch hosts not associated with a Person + * + * @param personObjectId May be null to fetch hosts not associated with a + * Person */ public FileSystemPersonSearchParam(Long personObjectId) { this.personObjectId = personObjectId; diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileTypeExtensionsSearchParams.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileTypeExtensionsSearchParams.java index fe875e1463..be0bf45de2 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileTypeExtensionsSearchParams.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileTypeExtensionsSearchParams.java @@ -25,6 +25,15 @@ import java.util.Objects; */ public class FileTypeExtensionsSearchParams { + private static final String TYPE_ID = "FILE_VIEWS_EXTENSION"; + + /** + * @return The type id for this search parameter. + */ + public static String getTypeId() { + return TYPE_ID; + } + private final FileExtSearchFilter filter; private final Long dataSourceId; diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileTypeMimeSearchParams.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileTypeMimeSearchParams.java index 8e95b18beb..0b89294e95 100755 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileTypeMimeSearchParams.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileTypeMimeSearchParams.java @@ -25,9 +25,18 @@ import java.util.Objects; */ public class FileTypeMimeSearchParams { + private static final String TYPE_ID = "FILE_VIEWS_MIME"; + + /** + * @return The type id for this search parameter. + */ + public static String getTypeId() { + return TYPE_ID; + } + private final String mimeType; private final Long dataSourceId; - + public FileTypeMimeSearchParams(String mimeType, Long dataSourceId) { this.mimeType = mimeType; this.dataSourceId = dataSourceId; @@ -70,5 +79,4 @@ public class FileTypeMimeSearchParams { return true; } - } diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileTypeSizeSearchParams.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileTypeSizeSearchParams.java index 1a1eb91917..898f1fca83 100755 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileTypeSizeSearchParams.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/FileTypeSizeSearchParams.java @@ -25,6 +25,14 @@ import java.util.Objects; */ public class FileTypeSizeSearchParams { + private static final String TYPE_ID = "FILE_VIEWS_SIZE"; + + /** + * @return The type id for this search parameter. + */ + public static String getTypeId() { + return TYPE_ID; + } private final FileSizeFilter sizeFilter; private final Long dataSourceId; diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/HashHitSearchParam.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/HashHitSearchParam.java index dc8495bffd..96299f9804 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/HashHitSearchParam.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/HashHitSearchParam.java @@ -24,7 +24,16 @@ import org.sleuthkit.datamodel.BlackboardArtifact; * Key for keyword hits in order to retrieve data from DAO. */ public class HashHitSearchParam extends AnalysisResultSetSearchParam { - + + private static final String TYPE_ID = "HASH_HIT"; + + /** + * @return The type id for this search parameter. + */ + public static String getTypeId() { + return TYPE_ID; + } + public HashHitSearchParam(Long dataSourceId, String setName) { super(BlackboardArtifact.Type.TSK_HASHSET_HIT, dataSourceId, setName); } diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/KeywordHitSearchParam.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/KeywordHitSearchParam.java index c0ce712b57..f796e4c4d2 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/KeywordHitSearchParam.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/KeywordHitSearchParam.java @@ -27,6 +27,15 @@ import org.sleuthkit.datamodel.TskData; */ public class KeywordHitSearchParam extends AnalysisResultSetSearchParam { + private static final String TYPE_ID = "KEYWORD_HIT"; + + /** + * @return The type id for this search parameter. + */ + public static String getTypeId() { + return TYPE_ID; + } + private final String keyword; private final String regex; private final TskData.KeywordSearchQueryType searchType; @@ -37,11 +46,11 @@ public class KeywordHitSearchParam extends AnalysisResultSetSearchParam { this.regex = regex; this.searchType = searchType; } - + public String getRegex() { return regex; } - + public String getKeyword() { return keyword; } @@ -83,6 +92,5 @@ public class KeywordHitSearchParam extends AnalysisResultSetSearchParam { } return super.equals(obj); } - - + } diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/KeywordMatchParams.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/KeywordMatchParams.java index 9bb39a0391..84f699850e 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/KeywordMatchParams.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/KeywordMatchParams.java @@ -25,6 +25,15 @@ import org.sleuthkit.datamodel.TskData; */ public class KeywordMatchParams { + private static final String TYPE_ID = "KEYWORD_MATCH"; + + /** + * @return The type id for this search parameter. + */ + public static String getTypeId() { + return TYPE_ID; + } + private final String setName; private final String searchTerm; private final String keywordMatch; diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/KeywordSearchTermParams.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/KeywordSearchTermParams.java index 2af0235d69..6d60da231f 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/KeywordSearchTermParams.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/KeywordSearchTermParams.java @@ -25,6 +25,15 @@ import org.sleuthkit.datamodel.TskData; */ public class KeywordSearchTermParams { + private static final String TYPE_ID = "KEYWORD_SEARCH_TERMS"; + + /** + * @return The type id for this search parameter. + */ + public static String getTypeId() { + return TYPE_ID; + } + private final String setName; private final String searchTerm; private final boolean hasChildren; diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/MainDAO.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/MainDAO.java index 44de97b935..148c606b46 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/MainDAO.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/MainDAO.java @@ -68,7 +68,8 @@ public class MainDAO extends AbstractDAO { Case.Events.OS_ACCOUNTS_ADDED.toString(), Case.Events.OS_ACCOUNTS_UPDATED.toString(), Case.Events.OS_ACCOUNTS_DELETED.toString(), - Case.Events.OS_ACCT_INSTANCES_ADDED.toString() + Case.Events.OS_ACCT_INSTANCES_ADDED.toString(), + Case.Events.DATA_SOURCE_ADDED.toString() ); private static final long WATCH_RESOLUTION_MILLIS = 30 * 1000; @@ -259,6 +260,7 @@ public class MainDAO extends AbstractDAO { return allDAOs.stream() .map(subDAO -> subDAO.processEvent(evt)) .flatMap(evts -> evts == null ? Stream.empty() : evts.stream()) + .filter(e -> e != null) .collect(Collectors.toSet()); } @@ -267,6 +269,7 @@ public class MainDAO extends AbstractDAO { return allDAOs.stream() .map((subDAO) -> subDAO.shouldRefreshTree()) .flatMap(evts -> evts == null ? Stream.empty() : evts.stream()) + .filter(e -> e != null) .collect(Collectors.toSet()); } @@ -280,6 +283,7 @@ public class MainDAO extends AbstractDAO { return daoStreamEvts.stream() .flatMap(evts -> evts == null ? Stream.empty() : evts.stream()) + .filter(evt -> evt != null) .collect(Collectors.toSet()); } diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/OsAccountsDAO.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/OsAccountsDAO.java index 78020b4dbf..ceace7b7f1 100755 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/OsAccountsDAO.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/OsAccountsDAO.java @@ -24,7 +24,6 @@ import com.google.common.cache.CacheBuilder; import java.beans.PropertyChangeEvent; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -187,13 +186,11 @@ public class OsAccountsDAO extends AbstractDAO { @Override Set handleIngestComplete() { - // GVDTODO return Collections.emptySet(); } @Override Set shouldRefreshTree() { - // GVDTODO return Collections.emptySet(); } @@ -202,9 +199,9 @@ public class OsAccountsDAO extends AbstractDAO { if (!OS_EVENTS.contains(evt.getPropertyName())) { return Collections.emptySet(); } - - this.searchParamsCache.invalidateAll(); - + + this.searchParamsCache.invalidateAll(); + return Collections.singleton(new OsAccountEvent()); } diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/OsAccountsSearchParams.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/OsAccountsSearchParams.java index e21db61826..61801fbb3d 100755 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/OsAccountsSearchParams.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/OsAccountsSearchParams.java @@ -25,8 +25,17 @@ import java.util.Objects; */ public class OsAccountsSearchParams { + private static final String TYPE_ID = "OS_ACCOUNTS"; + + /** + * @return The type id for this search parameter. + */ + public static String getTypeId() { + return TYPE_ID; + } + private final Long dataSourceId; - + public OsAccountsSearchParams(Long dataSourceId) { this.dataSourceId = dataSourceId; } diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/SubDAOUtils.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/SubDAOUtils.java index 53e2f7cfb3..2d0aff456a 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/SubDAOUtils.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/SubDAOUtils.java @@ -19,14 +19,16 @@ package org.sleuthkit.autopsy.mainui.datamodel; import com.google.common.cache.Cache; -import java.util.Collections; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentMap; +import java.util.function.BiFunction; import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; import org.apache.commons.lang3.tuple.Pair; +import org.sleuthkit.autopsy.mainui.datamodel.TreeResultsDTO.TreeDisplayCount; +import org.sleuthkit.autopsy.mainui.datamodel.TreeResultsDTO.TreeItemDTO; import org.sleuthkit.autopsy.mainui.datamodel.events.TreeCounts; import org.sleuthkit.autopsy.mainui.datamodel.events.TreeEvent; @@ -46,32 +48,28 @@ public class SubDAOUtils { * no data source filtering). * @param itemDataSourceMapping The event digest. */ - static void invalidateKeys(Cache, ?> cache, Function> getKeys, Map> itemDataSourceMapping) { - invalidateKeys(cache, getKeys, Collections.singletonList(itemDataSourceMapping)); + static void invalidateKeys(Cache, ?> cache, Function> getKeys, Map> itemDsMapping) { + invalidateKeys(cache, (keyParams) -> { + Pair pairItems = getKeys.apply(keyParams); + T searchParamsKey = pairItems.getLeft(); + Long searchParamsDsId = pairItems.getRight(); + Set dsIds = itemDsMapping.get(searchParamsKey); + return (dsIds != null && (searchParamsDsId == null || dsIds.contains(searchParamsDsId))); + }); } /** - * Using a digest of event information, clears keys in a cache that may be - * effected by events. + * Determines what keys should be kept in the cache while iterating through + * all the keys. * - * @param cache The cache. - * @param getKeys Using a key from a cache, provides a tuple - * of the relevant key in the data source - * mapping and the data source id (or null if - * no data source filtering). - * @param itemDataSourceMapping The list of event digests. + * @param cache The cache. + * @param shouldInvalidate If the key should be removed from the cache. */ - static void invalidateKeys(Cache, ?> cache, Function> getKeys, List>> itemDataSourceMapping) { + static void invalidateKeys(Cache, ?> cache, Predicate shouldInvalidate) { ConcurrentMap, ?> concurrentMap = cache.asMap(); concurrentMap.forEach((k, v) -> { - Pair pairItems = getKeys.apply(k.getParamData()); - T searchParamsKey = pairItems.getLeft(); - Long searchParamsDsId = pairItems.getRight(); - for (Map> itemDsMapping : itemDataSourceMapping) { - Set dsIds = itemDsMapping.get(searchParamsKey); - if (dsIds != null && (searchParamsDsId == null || dsIds.contains(searchParamsDsId))) { - concurrentMap.remove(k); - } + if (shouldInvalidate.test(k.getParamData())) { + concurrentMap.remove(k); } }); } @@ -86,9 +84,9 @@ public class SubDAOUtils { * * @return The generated tree events. */ - static Set getIngestCompleteEvents(TreeCounts treeCounts, Function> converter) { + static Set getIngestCompleteEvents(TreeCounts treeCounts, BiFunction> converter) { return treeCounts.flushEvents().stream() - .map(daoEvt -> new TreeEvent(converter.apply(daoEvt), true)) + .map(daoEvt -> new TreeEvent(converter.apply(daoEvt, TreeDisplayCount.UNSPECIFIED), true)) .collect(Collectors.toSet()); } @@ -102,9 +100,9 @@ public class SubDAOUtils { * * @return The generated tree events. */ - static Set getRefreshEvents(TreeCounts treeCounts, Function> converter) { + static Set getRefreshEvents(TreeCounts treeCounts, BiFunction> converter) { return treeCounts.getEventTimeouts().stream() - .map(daoEvt -> new TreeEvent(converter.apply(daoEvt), true)) + .map(daoEvt -> new TreeEvent(converter.apply(daoEvt, TreeDisplayCount.UNSPECIFIED), true)) .collect(Collectors.toSet()); } } diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/TagsDAO.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/TagsDAO.java index 37c23ef375..b794f26bca 100755 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/TagsDAO.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/TagsDAO.java @@ -27,20 +27,14 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.Objects; import java.util.Set; -import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; -import org.apache.commons.lang3.tuple.Triple; import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.Case; @@ -52,7 +46,9 @@ import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.coreutils.TimeZoneUtils; import org.sleuthkit.autopsy.mainui.datamodel.TagsSearchParams.TagType; +import org.sleuthkit.autopsy.mainui.datamodel.TreeResultsDTO.TreeItemDTO; import org.sleuthkit.autopsy.mainui.datamodel.events.TagsEvent; +import org.sleuthkit.autopsy.mainui.datamodel.events.TreeCounts; import org.sleuthkit.autopsy.mainui.datamodel.events.TreeEvent; import org.sleuthkit.autopsy.mainui.nodes.DAOFetcher; import org.sleuthkit.datamodel.AbstractFile; @@ -90,7 +86,6 @@ public class TagsDAO extends AbstractDAO { private static final int CACHE_SIZE = 5; // rule of thumb: 5 entries times number of cached SearchParams sub-types private static final long CACHE_DURATION = 2; private static final TimeUnit CACHE_DURATION_UNITS = TimeUnit.MINUTES; - private final Cache, SearchResultsDTO> searchParamsCache = CacheBuilder.newBuilder().maximumSize(CACHE_SIZE).expireAfterAccess(CACHE_DURATION, CACHE_DURATION_UNITS).build(); private static final String USER_NAME_PROPERTY = "user.name"; //NON-NLS @@ -129,6 +124,11 @@ public class TagsDAO extends AbstractDAO { return new ColumnKey(name, name, Bundle.TagsDAO_fileColumns_noDescription()); } + private final Cache, SearchResultsDTO> searchParamsCache + = CacheBuilder.newBuilder().maximumSize(CACHE_SIZE).expireAfterAccess(CACHE_DURATION, CACHE_DURATION_UNITS).build(); + + private final TreeCounts treeCounts = new TreeCounts<>(); + public SearchResultsDTO getTags(TagsSearchParams key, long startItem, Long maxCount) throws ExecutionException, IllegalArgumentException { if (key.getTagName() == null) { throw new IllegalArgumentException("Must have non-null tag name"); @@ -301,71 +301,59 @@ public class TagsDAO extends AbstractDAO { } TagsEvent tagEvt = (TagsEvent) daoEvt; - return (tagParams.getTagName().getId() == tagEvt.getTagNameId() + return (Objects.equals(tagParams.getTagName(), tagEvt.getTagName()) && tagParams.getTagType().equals(tagEvt.getTagType()) && (tagParams.getDataSourceId() == null || tagEvt.getDataSourceId() == null || tagParams.getDataSourceId() == tagEvt.getDataSourceId())); } - @Override - void clearCaches() { - this.searchParamsCache.invalidateAll(); + private TreeItemDTO getTreeItem(TagsEvent evt, TreeResultsDTO.TreeDisplayCount count) { + return new TreeItemDTO<>( + TagsSearchParams.getTypeId(), + new TagsSearchParams(evt.getTagName(), evt.getTagType(), evt.getDataSourceId()), + evt.getTagName().getId(), + evt.getTagName().getDisplayName(), + count); } @Override - Set handleIngestComplete() { - // GVDTODO - return Collections.emptySet(); + void clearCaches() { + this.searchParamsCache.invalidateAll(); + handleIngestComplete(); + } + + @Override + Set handleIngestComplete() { + return SubDAOUtils.getIngestCompleteEvents(this.treeCounts, (evt, count) -> getTreeItem(evt, count)); } @Override Set shouldRefreshTree() { - // GVDTODO - return Collections.emptySet(); + return SubDAOUtils.getRefreshEvents(this.treeCounts, (evt, count) -> getTreeItem(evt, count)); } @Override Set processEvent(PropertyChangeEvent evt) { - // GVDTODO this may be rewritten simpler now that it isn't processing a list of events - Map, Set>> mapping = new HashMap<>(); - - // tag type, tag name id, data source id (or null if unknown) - Triple data = getTagData(evt); - if (data != null) { - mapping.computeIfAbsent(Pair.of(data.getLeft(), data.getMiddle()), k -> new HashSet<>()) - .add(Optional.ofNullable(data.getRight())); - } - - - // don't continue if no mapping entries - if (mapping.isEmpty()) { + TagsEvent data = getTagData(evt); + if (data == null) { return Collections.emptySet(); } - ConcurrentMap, SearchResultsDTO> concurrentMap = this.searchParamsCache.asMap(); - concurrentMap.forEach((k, v) -> { - TagsSearchParams paramData = k.getParamData(); - Set> affectedDataSources = mapping.get(Pair.of(paramData.getTagType(), paramData.getTagName().getId())); - // we only clear key if the tag name / type line up and either the parameters data source wasn't specified, - // there is a wild card data source for the event, or the data source is contained in the list of data sources - // affected by the event - if (affectedDataSources != null - && (paramData.getDataSourceId() == null - || affectedDataSources.contains(Optional.empty()) - || affectedDataSources.contains(Optional.of(paramData.getDataSourceId())))) { - concurrentMap.remove(k); - } + SubDAOUtils.invalidateKeys(this.searchParamsCache, (searchParams) -> { + return (Objects.equals(searchParams.getTagType(), data.getTagType()) + && Objects.equals(searchParams.getTagName(), data.getTagName()) + && (searchParams.getDataSourceId() == null || Objects.equals(searchParams.getDataSourceId(), data.getDataSourceId()))); }); - return mapping.entrySet().stream() - .flatMap(entry -> { - TagType tagType = entry.getKey().getLeft(); - Long tagNameId = entry.getKey().getRight(); + Collection daoEvents = Collections.singletonList(data); - return entry.getValue().stream() - .map((dsIdOpt) -> new TagsEvent(tagType, tagNameId, dsIdOpt.orElse(null))); - }) + Collection treeEvents = this.treeCounts.enqueueAll(daoEvents).stream() + .map(arEvt -> new TreeEvent(getTreeItem(arEvt, TreeResultsDTO.TreeDisplayCount.INDETERMINATE), false)) + .collect(Collectors.toList()); + + return Stream.of(daoEvents, treeEvents) + .flatMap(lst -> lst.stream()) .collect(Collectors.toSet()); } @@ -378,21 +366,21 @@ public class TagsDAO extends AbstractDAO { * @return tag type, tag name id, data source id (or null if none determined * from event). */ - private Triple getTagData(PropertyChangeEvent evt) { + private TagsEvent getTagData(PropertyChangeEvent evt) { if (evt instanceof BlackBoardArtifactTagAddedEvent) { BlackBoardArtifactTagAddedEvent event = (BlackBoardArtifactTagAddedEvent) evt; // ensure tag added event has a valid content id if (event.getAddedTag() != null && event.getAddedTag().getContent() != null && event.getAddedTag().getArtifact() != null) { - return Triple.of(TagType.RESULT, event.getAddedTag().getName().getId(), event.getAddedTag().getArtifact().getDataSourceObjectID()); + return new TagsEvent(TagType.RESULT, event.getAddedTag().getName(), event.getAddedTag().getArtifact().getDataSourceObjectID()); } } else if (evt instanceof BlackBoardArtifactTagDeletedEvent) { BlackBoardArtifactTagDeletedEvent event = (BlackBoardArtifactTagDeletedEvent) evt; BlackBoardArtifactTagDeletedEvent.DeletedBlackboardArtifactTagInfo deletedTagInfo = event.getDeletedTagInfo(); if (deletedTagInfo != null) { - return Triple.of(TagType.RESULT, deletedTagInfo.getName().getId(), null); + return new TagsEvent(TagType.RESULT, deletedTagInfo.getName(), null); } } else if (evt instanceof ContentTagAddedEvent) { ContentTagAddedEvent event = (ContentTagAddedEvent) evt; @@ -400,14 +388,14 @@ public class TagsDAO extends AbstractDAO { if (event.getAddedTag() != null && event.getAddedTag().getContent() != null) { Content content = event.getAddedTag().getContent(); Long dsId = content instanceof AbstractFile ? ((AbstractFile) content).getDataSourceObjectId() : null; - return Triple.of(TagType.FILE, event.getAddedTag().getName().getId(), dsId); + return new TagsEvent(TagType.FILE, event.getAddedTag().getName(), dsId); } } else if (evt instanceof ContentTagDeletedEvent) { ContentTagDeletedEvent event = (ContentTagDeletedEvent) evt; // ensure tag deleted event has a valid content id ContentTagDeletedEvent.DeletedContentTagInfo deletedTagInfo = event.getDeletedTagInfo(); if (deletedTagInfo != null) { - return Triple.of(TagType.FILE, deletedTagInfo.getName().getId(), null); + return new TagsEvent(TagType.FILE, deletedTagInfo.getName(), null); } } return null; diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/TagsSearchParams.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/TagsSearchParams.java index 2f239047cd..5f5d862cf3 100755 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/TagsSearchParams.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/TagsSearchParams.java @@ -25,7 +25,16 @@ import org.sleuthkit.datamodel.TagName; * Key for accessing data about tags from the DAO. */ public class TagsSearchParams { - + + private static final String TYPE_ID = "TAG"; + + /** + * @return The type id for this search parameter. + */ + public static String getTypeId() { + return TYPE_ID; + } + public enum TagType { FILE, RESULT; @@ -34,7 +43,7 @@ public class TagsSearchParams { private final TagType type; private final TagName tagName; private final Long dataSourceId; - + public TagsSearchParams(TagName tagName, TagType type, Long dataSourceId) { this.tagName = tagName; this.type = type; diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/TreeResultsDTO.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/TreeResultsDTO.java index 78a310530d..d2a92871bf 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/TreeResultsDTO.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/TreeResultsDTO.java @@ -98,6 +98,7 @@ public class TreeResultsDTO { case INDETERMINATE: return " (...)"; case NOT_SHOWN: + case UNSPECIFIED: default: return ""; } diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/ViewsDAO.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/ViewsDAO.java index 1070fd9907..f4e856b82e 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/ViewsDAO.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/ViewsDAO.java @@ -21,9 +21,11 @@ package org.sleuthkit.autopsy.mainui.datamodel; import org.sleuthkit.autopsy.mainui.datamodel.events.DAOEvent; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ImmutableSet; import java.beans.PropertyChangeEvent; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -33,9 +35,9 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Set; -import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -53,6 +55,7 @@ import org.sleuthkit.autopsy.mainui.datamodel.events.DAOEventUtils; import org.sleuthkit.autopsy.mainui.datamodel.events.FileTypeExtensionsEvent; import org.sleuthkit.autopsy.mainui.datamodel.events.FileTypeMimeEvent; import org.sleuthkit.autopsy.mainui.datamodel.events.FileTypeSizeEvent; +import org.sleuthkit.autopsy.mainui.datamodel.events.TreeCounts; import org.sleuthkit.autopsy.mainui.datamodel.events.TreeEvent; import org.sleuthkit.autopsy.mainui.nodes.DAOFetcher; import org.sleuthkit.datamodel.AbstractFile; @@ -60,6 +63,7 @@ import org.sleuthkit.datamodel.CaseDbAccessManager.CaseDbPreparedStatement; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; +import org.sleuthkit.datamodel.TskData.TSK_FS_NAME_TYPE_ENUM; /** * Provides information to populate the results viewer for data in the views @@ -72,7 +76,14 @@ public class ViewsDAO extends AbstractDAO { private static final int CACHE_SIZE = 15; // rule of thumb: 5 entries times number of cached SearchParams sub-types private static final long CACHE_DURATION = 2; private static final TimeUnit CACHE_DURATION_UNITS = TimeUnit.MINUTES; - private final Cache, SearchResultsDTO> searchParamsCache = CacheBuilder.newBuilder().maximumSize(CACHE_SIZE).expireAfterAccess(CACHE_DURATION, CACHE_DURATION_UNITS).build(); + private static final Map> EXTENSION_FILTER_MAP + = Stream.of((FileExtSearchFilter[]) FileExtRootFilter.values(), FileExtDocumentFilter.values(), FileExtExecutableFilter.values()) + .flatMap(arr -> Stream.of(arr)) + .flatMap(filter -> filter.getFilter().stream().map(ext -> Pair.of(ext, filter))) + .collect(Collectors.groupingBy( + pair -> pair.getKey(), + Collectors.mapping(pair -> pair.getValue(), + Collectors.toSet()))); private static final String FILE_VIEW_EXT_TYPE_ID = "FILE_VIEW_BY_EXT"; @@ -86,6 +97,9 @@ public class ViewsDAO extends AbstractDAO { return instance; } + private final Cache, SearchResultsDTO> searchParamsCache = CacheBuilder.newBuilder().maximumSize(CACHE_SIZE).expireAfterAccess(CACHE_DURATION, CACHE_DURATION_UNITS).build(); + private final TreeCounts treeCounts = new TreeCounts<>(); + private SleuthkitCase getCase() throws NoCurrentCaseException { return Case.getCurrentCaseThrows().getSleuthkitCase(); } @@ -97,7 +111,7 @@ public class ViewsDAO extends AbstractDAO { throw new IllegalArgumentException("Data source id must be greater than 0 or null"); } - SearchParams searchParams = new SearchParams<>(key, startItem, maxCount); + SearchParams searchParams = new SearchParams<>(key, startItem, maxCount); return searchParamsCache.get(searchParams, () -> fetchExtensionSearchResultsDTOs(key.getFilter(), key.getDataSourceId(), startItem, maxCount)); } @@ -108,7 +122,7 @@ public class ViewsDAO extends AbstractDAO { throw new IllegalArgumentException("Data source id must be greater than 0 or null"); } - SearchParams searchParams = new SearchParams<>(key, startItem, maxCount); + SearchParams searchParams = new SearchParams<>(key, startItem, maxCount); return searchParamsCache.get(searchParams, () -> fetchMimeSearchResultsDTOs(key.getMimeType(), key.getDataSourceId(), startItem, maxCount)); } @@ -119,7 +133,7 @@ public class ViewsDAO extends AbstractDAO { throw new IllegalArgumentException("Data source id must be greater than 0 or null"); } - SearchParams searchParams = new SearchParams<>(key, startItem, maxCount); + SearchParams searchParams = new SearchParams<>(key, startItem, maxCount); return searchParamsCache.get(searchParams, () -> fetchSizeSearchResultsDTOs(key.getSizeFilter(), key.getDataSourceId(), startItem, maxCount)); } @@ -129,9 +143,8 @@ public class ViewsDAO extends AbstractDAO { } FileTypeExtensionsEvent extEvt = (FileTypeExtensionsEvent) eventData; - String extension = extEvt.getExtension().toLowerCase(); - return key.getFilter().getFilter().contains(extension) - && (key.getDataSourceId() == null || key.getDataSourceId().equals(extEvt.getDataSourceId())); + return (extEvt.getExtensionFilter() == null || key.getFilter().equals(extEvt.getExtensionFilter())) + && (key.getDataSourceId() == null || extEvt.getDataSourceId() == null || key.getDataSourceId().equals(extEvt.getDataSourceId())); } private boolean isFilesByMimeInvalidating(FileTypeMimeSearchParams key, DAOEvent eventData) { @@ -150,8 +163,8 @@ public class ViewsDAO extends AbstractDAO { } FileTypeSizeEvent sizeEvt = (FileTypeSizeEvent) eventData; - return sizeEvt.getSizeFilter().equals(key.getSizeFilter()) - && (key.getDataSourceId() == null || Objects.equals(key.getDataSourceId(), sizeEvt.getDataSourceId())); + return (sizeEvt.getSizeFilter() == null || sizeEvt.getSizeFilter().equals(key.getSizeFilter())) + && (key.getDataSourceId() == null || sizeEvt.getDataSourceId() == null || Objects.equals(key.getDataSourceId(), sizeEvt.getDataSourceId())); } /** @@ -184,6 +197,22 @@ public class ViewsDAO extends AbstractDAO { .collect(Collectors.joining(", ")) + ")"; } + /** + * @return If user preference of hide known files, returns sql and clause to + * hide known files or returns empty string otherwise. + */ + private String getHideKnownAndClause() { + return (hideKnownFilesInViewsTree() ? (" AND (known IS NULL OR known <> " + TskData.FileKnown.KNOWN.getFileKnownValue() + ") ") : ""); + } + + /** + * @return A clause (no 'and' or 'where' prefixed) indicating the dir_type + * is regular. + */ + private String getRegDirTypeClause() { + return "(dir_type = " + TskData.TSK_FS_NAME_TYPE_ENUM.REG.getValue() + ")"; + } + /** * Returns a clause that will filter out files that aren't to be counted in * the file extensions view. @@ -191,8 +220,7 @@ public class ViewsDAO extends AbstractDAO { * @return The filter that will need to be proceeded with 'where' or 'and'. */ private String getBaseFileExtensionFilter() { - return "(dir_type = " + TskData.TSK_FS_NAME_TYPE_ENUM.REG.getValue() + ")" - + (hideKnownFilesInViewsTree() ? (" AND (known IS NULL OR known <> " + TskData.FileKnown.KNOWN.getFileKnownValue() + ")") : ""); + return getRegDirTypeClause() + getHideKnownAndClause(); } /** @@ -212,6 +240,22 @@ public class ViewsDAO extends AbstractDAO { return whereClause; } + /** + * @return The TSK_DB_FILES_TYPE_ENUm values allowed for mime type view + * items. + */ + private Set getMimeDbFilesTypes() { + return Stream.of( + TskData.TSK_DB_FILES_TYPE_ENUM.FS, + TskData.TSK_DB_FILES_TYPE_ENUM.CARVED, + TskData.TSK_DB_FILES_TYPE_ENUM.DERIVED, + TskData.TSK_DB_FILES_TYPE_ENUM.LAYOUT_FILE, + TskData.TSK_DB_FILES_TYPE_ENUM.LOCAL, + (hideSlackFilesInViewsTree() ? null : (TskData.TSK_DB_FILES_TYPE_ENUM.SLACK))) + .filter(ordinal -> ordinal != null) + .collect(Collectors.toSet()); + } + /** * Returns a statement to be proceeded with 'where' or 'and' that will * filter out results that should not be viewed in mime types view. @@ -219,15 +263,10 @@ public class ViewsDAO extends AbstractDAO { * @return A statement to be proceeded with 'and' or 'where'. */ private String getBaseFileMimeFilter() { - return "(dir_type = " + TskData.TSK_FS_NAME_TYPE_ENUM.REG.getValue() + ")" - + (hideKnownFilesInViewsTree() ? (" AND (known IS NULL OR known != " + TskData.FileKnown.KNOWN.getFileKnownValue() + ")") : "") + return getRegDirTypeClause() + + getHideKnownAndClause() + " AND (type IN (" - + TskData.TSK_DB_FILES_TYPE_ENUM.FS.ordinal() + "," - + TskData.TSK_DB_FILES_TYPE_ENUM.CARVED.ordinal() + "," - + TskData.TSK_DB_FILES_TYPE_ENUM.DERIVED.ordinal() + "," - + TskData.TSK_DB_FILES_TYPE_ENUM.LAYOUT_FILE.ordinal() + "," - + TskData.TSK_DB_FILES_TYPE_ENUM.LOCAL.ordinal() - + (hideSlackFilesInViewsTree() ? "" : ("," + TskData.TSK_DB_FILES_TYPE_ENUM.SLACK.ordinal())) + + getMimeDbFilesTypes().stream().map(v -> Integer.toString(v.ordinal())).collect(Collectors.joining(", ")) + "))"; } @@ -270,8 +309,7 @@ public class ViewsDAO extends AbstractDAO { */ private String getBaseFileSizeFilter() { // Ignore unallocated block files. - return "(type != " + TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS.getFileType() + ")" - + ((hideKnownFilesInViewsTree() ? (" AND (known IS NULL OR known != " + TskData.FileKnown.KNOWN.getFileKnownValue() + ")") : "")); //NON-NLS + return "(type != " + TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS.getFileType() + ")" + getHideKnownAndClause(); } /** @@ -306,6 +344,22 @@ public class ViewsDAO extends AbstractDAO { * @throws ExecutionException */ public TreeResultsDTO getFileExtCounts(Collection filters, Long dataSourceId) throws IllegalArgumentException, ExecutionException { + Set indeterminateFilters = new HashSet<>(); + for (DAOEvent evt : this.treeCounts.getEnqueued()) { + if (evt instanceof FileTypeExtensionsEvent) { + FileTypeExtensionsEvent extEvt = (FileTypeExtensionsEvent) evt; + if (dataSourceId == null || extEvt.getDataSourceId() == null || Objects.equals(extEvt.getDataSourceId(), dataSourceId)) { + if (extEvt.getExtensionFilter() == null) { + // add all filters if extension filter is null and keep going + indeterminateFilters.addAll(filters); + break; + } else if (filters.contains(extEvt.getExtensionFilter())) { + indeterminateFilters.add(extEvt.getExtensionFilter()); + } + } + } + } + Map whereClauses = filters.stream() .collect(Collectors.toMap( filter -> filter, @@ -315,12 +369,11 @@ public class ViewsDAO extends AbstractDAO { List> treeList = countsByFilter.entrySet().stream() .map(entry -> { - return new TreeItemDTO<>( - "FILE_EXT", - new FileTypeExtensionsSearchParams(entry.getKey(), dataSourceId), - entry.getKey(), - entry.getKey().getDisplayName(), - TreeDisplayCount.getDeterminate(entry.getValue())); + TreeDisplayCount displayCount = indeterminateFilters.contains(entry.getKey()) + ? TreeDisplayCount.INDETERMINATE + : TreeDisplayCount.getDeterminate(entry.getValue()); + + return createExtensionTreeItem(entry.getKey(), dataSourceId, displayCount); }) .sorted((a, b) -> a.getDisplayName().compareToIgnoreCase(b.getDisplayName())) .collect(Collectors.toList()); @@ -328,6 +381,24 @@ public class ViewsDAO extends AbstractDAO { return new TreeResultsDTO<>(treeList); } + /** + * Creates an extension tree item. + * + * @param filter The extension filter. + * @param dataSourceId The data source id or null. + * @param displayCount The count to display. + * + * @return The extension tree item. + */ + private TreeItemDTO createExtensionTreeItem(FileExtSearchFilter filter, Long dataSourceId, TreeDisplayCount displayCount) { + return new TreeItemDTO<>( + FileTypeExtensionsSearchParams.getTypeId(), + new FileTypeExtensionsSearchParams(filter, dataSourceId), + filter, + filter == null ? "" : filter.getDisplayName(), + displayCount); + } + /** * Returns counts for file size categories. * @@ -340,6 +411,22 @@ public class ViewsDAO extends AbstractDAO { * @throws ExecutionException */ public TreeResultsDTO getFileSizeCounts(Long dataSourceId) throws IllegalArgumentException, ExecutionException { + Set indeterminateFilters = new HashSet<>(); + for (DAOEvent evt : this.treeCounts.getEnqueued()) { + if (evt instanceof FileTypeSizeEvent) { + FileTypeSizeEvent sizeEvt = (FileTypeSizeEvent) evt; + if (dataSourceId == null || sizeEvt.getDataSourceId() == null || Objects.equals(sizeEvt.getDataSourceId(), dataSourceId)) { + if (sizeEvt.getSizeFilter() == null) { + // if null size filter, indicates full refresh and all file sizes need refresh. + indeterminateFilters.addAll(Arrays.asList(FileSizeFilter.values())); + break; + } else { + indeterminateFilters.add(sizeEvt.getSizeFilter()); + } + } + } + } + Map whereClauses = Stream.of(FileSizeFilter.values()) .collect(Collectors.toMap( filter -> filter, @@ -349,12 +436,11 @@ public class ViewsDAO extends AbstractDAO { List> treeList = countsByFilter.entrySet().stream() .map(entry -> { - return new TreeItemDTO<>( - "FILE_SIZE", - new FileTypeSizeSearchParams(entry.getKey(), dataSourceId), - entry.getKey(), - entry.getKey().getDisplayName(), - TreeDisplayCount.getDeterminate(entry.getValue())); + TreeDisplayCount displayCount = indeterminateFilters.contains(entry.getKey()) + ? TreeDisplayCount.INDETERMINATE + : TreeDisplayCount.getDeterminate(entry.getValue()); + + return createSizeTreeItem(entry.getKey(), dataSourceId, displayCount); }) .sorted((a, b) -> a.getDisplayName().compareToIgnoreCase(b.getDisplayName())) .collect(Collectors.toList()); @@ -362,6 +448,24 @@ public class ViewsDAO extends AbstractDAO { return new TreeResultsDTO<>(treeList); } + /** + * Creates a size tree item. + * + * @param filter The file size filter. + * @param dataSourceId The data source id. + * @param displayCount The display count. + * + * @return The tree item. + */ + private TreeItemDTO createSizeTreeItem(FileSizeFilter filter, Long dataSourceId, TreeDisplayCount displayCount) { + return new TreeItemDTO<>( + FileTypeSizeSearchParams.getTypeId(), + new FileTypeSizeSearchParams(filter, dataSourceId), + filter, + filter == null ? "" : filter.getDisplayName(), + displayCount); + } + /** * Returns counts for file mime type categories. * @@ -379,6 +483,22 @@ public class ViewsDAO extends AbstractDAO { String prefixWithSlash = StringUtils.isNotBlank(prefix) ? prefix.replaceAll("/", "") + "/" : null; String likeItem = StringUtils.isNotBlank(prefixWithSlash) ? prefixWithSlash.replaceAll("%", "") + "%" : null; + Set indeterminateMimeTypes = new HashSet<>(); + for (DAOEvent evt : this.treeCounts.getEnqueued()) { + if (evt instanceof FileTypeMimeEvent) { + FileTypeMimeEvent mimeEvt = (FileTypeMimeEvent) evt; + if ((dataSourceId == null || Objects.equals(mimeEvt.getDataSourceId(), dataSourceId)) + && (prefixWithSlash == null || mimeEvt.getMimeType().startsWith(prefixWithSlash))) { + + String mimePortion = prefixWithSlash != null + ? mimeEvt.getMimeType().substring(prefixWithSlash.length()) + : mimeEvt.getMimeType().substring(0, mimeEvt.getMimeType().indexOf("/")); + + indeterminateMimeTypes.add(mimePortion); + } + } + } + String baseFilter = "WHERE " + getBaseFileMimeFilter() + getDataSourceAndClause(dataSourceId) + (StringUtils.isNotBlank(prefix) ? " AND mime_type LIKE ? " : " AND mime_type IS NOT NULL "); @@ -434,12 +554,11 @@ public class ViewsDAO extends AbstractDAO { ? entry.getKey().substring(prefixWithSlash.length()) : entry.getKey(); - return new TreeItemDTO<>( - "FILE_MIME_TYPE", - new FileTypeMimeSearchParams(entry.getKey(), dataSourceId), - name, - name, - TreeDisplayCount.getDeterminate(entry.getValue())); + TreeDisplayCount displayCount = indeterminateMimeTypes.contains(name) + ? TreeDisplayCount.INDETERMINATE + : TreeDisplayCount.getDeterminate(entry.getValue()); + + return createMimeTreeItem(entry.getKey(), name, dataSourceId, displayCount); }) .sorted((a, b) -> stringCompare(a.getSearchParams().getMimeType(), b.getSearchParams().getMimeType())) .collect(Collectors.toList()); @@ -453,6 +572,26 @@ public class ViewsDAO extends AbstractDAO { } } + /** + * Creates a mime type tree item. + * + * @param fullMime The full mime type. + * @param mimeName The mime type segment that will be displayed (suffix + * or prefix). + * @param dataSourceId The data source id. + * @param displayCount The count to display. + * + * @return The created tree item. + */ + private TreeItemDTO createMimeTreeItem(String fullMime, String mimeName, Long dataSourceId, TreeDisplayCount displayCount) { + return new TreeItemDTO<>( + FileTypeMimeSearchParams.getTypeId(), + new FileTypeMimeSearchParams(fullMime, dataSourceId), + mimeName, + mimeName, + displayCount); + } + /** * Provides case insensitive comparator integer for strings that may be * null. @@ -612,11 +751,6 @@ public class ViewsDAO extends AbstractDAO { return new BaseSearchResultsDTO(FILE_VIEW_EXT_TYPE_ID, displayName, FileSystemColumnUtils.getColumnKeysForAbstractfile(), fileRows, AbstractFile.class.getName(), startItem, totalResultsCount); } - @Override - void clearCaches() { - this.searchParamsCache.invalidateAll(); - } - private Pair getMimePieces(String mimeType) { int idx = mimeType.indexOf("/"); String mimePrefix = idx > 0 ? mimeType.substring(0, idx) : mimeType; @@ -624,167 +758,212 @@ public class ViewsDAO extends AbstractDAO { return Pair.of(mimePrefix, mimeSuffix); } + private TreeItemDTO createTreeItem(DAOEvent daoEvent, TreeDisplayCount count) { + if (daoEvent instanceof FileTypeExtensionsEvent) { + FileTypeExtensionsEvent extEvt = (FileTypeExtensionsEvent) daoEvent; + return createExtensionTreeItem(extEvt.getExtensionFilter(), extEvt.getDataSourceId(), count); + } else if (daoEvent instanceof FileTypeMimeEvent) { + FileTypeMimeEvent mimeEvt = (FileTypeMimeEvent) daoEvent; + Pair mimePieces = getMimePieces(mimeEvt.getMimeType()); + String mimeName = mimePieces.getRight() == null ? mimePieces.getLeft() : mimePieces.getRight(); + return createMimeTreeItem(mimeEvt.getMimeType(), mimeName, mimeEvt.getDataSourceId(), count); + } else if (daoEvent instanceof FileTypeSizeEvent) { + FileTypeSizeEvent sizeEvt = (FileTypeSizeEvent) daoEvent; + return createSizeTreeItem(sizeEvt.getSizeFilter(), sizeEvt.getDataSourceId(), count); + } else { + return null; + } + } + @Override - Set handleIngestComplete() { - // GVDTODO - return Collections.emptySet(); + void clearCaches() { + this.searchParamsCache.invalidateAll(); + handleIngestComplete(); + } + + @Override + Set handleIngestComplete() { + SubDAOUtils.invalidateKeys(this.searchParamsCache, + (searchParams) -> searchParamsMatchEvent(null, null, null, null, true, searchParams)); + + Set treeEvts = SubDAOUtils.getIngestCompleteEvents(this.treeCounts, + (daoEvt, count) -> createTreeItem(daoEvt, count)); + + Set fileViewRefreshEvents = getFileViewRefreshEvents(null); + + List fileViewRefreshTreeEvents = fileViewRefreshEvents.stream() + .map(evt -> new TreeEvent(createTreeItem(evt, TreeDisplayCount.UNSPECIFIED), true)) + .collect(Collectors.toList()); + + return Stream.of(treeEvts, fileViewRefreshEvents, fileViewRefreshTreeEvents) + .flatMap(c -> c.stream()) + .collect(Collectors.toSet()); } @Override Set shouldRefreshTree() { - // GVDTODO - return Collections.emptySet(); + return SubDAOUtils.getRefreshEvents(this.treeCounts, + (daoEvt, count) -> createTreeItem(daoEvt, count)); } @Override Set processEvent(PropertyChangeEvent evt) { - // GVDTODO maps may not be necessary now that this isn't processing a list of events. - Map> fileExtensionDsMap = new HashMap<>(); - Map>> mimeTypeDsMap = new HashMap<>(); - Map> fileSizeDsMap = new HashMap<>(); + Long dsId = null; + boolean dataSourceAdded = false; + Set evtExtFilters = null; + String evtMimeType = null; + FileSizeFilter evtFileSize = null; - AbstractFile af = DAOEventUtils.getFileFromFileEvent(evt); - if (af == null) { - return Collections.emptySet(); - } + if (Case.Events.DATA_SOURCE_ADDED.toString().equals(evt.getPropertyName())) { + dsId = evt.getNewValue() instanceof Long ? (Long) evt.getNewValue() : null; + dataSourceAdded = true; + } else { + AbstractFile af = DAOEventUtils.getFileFromFileEvent(evt); + if (af == null) { + return Collections.emptySet(); + } else if (hideKnownFilesInViewsTree() && TskData.FileKnown.KNOWN.equals(af.getKnown())) { + return Collections.emptySet(); + } - // create an extension mapping if extension present - if (!StringUtils.isBlank(af.getNameExtension())) { - fileExtensionDsMap - .computeIfAbsent("." + af.getNameExtension(), (k) -> new HashSet<>()) - .add(af.getDataSourceObjectId()); - } + dsId = af.getDataSourceObjectId(); - // create a mime type mapping if mime type present - if (!StringUtils.isBlank(af.getMIMEType())) { - Pair mimePieces = getMimePieces(af.getMIMEType()); - mimeTypeDsMap - .computeIfAbsent(mimePieces.getKey(), (k) -> new HashMap<>()) - .computeIfAbsent(mimePieces.getValue(), (k) -> new HashSet<>()) - .add(af.getDataSourceObjectId()); - } + // create an extension mapping if extension present + if (!StringUtils.isBlank(af.getNameExtension()) && TSK_FS_NAME_TYPE_ENUM.REG.equals(af.getDirType())) { + evtExtFilters = EXTENSION_FILTER_MAP.getOrDefault("." + af.getNameExtension(), Collections.emptySet()); + } - // create a size mapping if size present - FileSizeFilter sizeFilter = Stream.of(FileSizeFilter.values()) - .filter(filter -> af.getSize() >= filter.getMinBound() && (filter.getMaxBound() == null || af.getSize() < filter.getMaxBound())) - .findFirst() - .orElse(null); + // create a mime type mapping if mime type present + if (!StringUtils.isBlank(af.getMIMEType()) && TSK_FS_NAME_TYPE_ENUM.REG.equals(af.getDirType()) && getMimeDbFilesTypes().contains(af.getType())) { + evtMimeType = af.getMIMEType(); + } - if (sizeFilter != null) { - fileSizeDsMap - .computeIfAbsent(sizeFilter, (k) -> new HashSet<>()) - .add(af.getDataSourceObjectId()); - } + // create a size mapping if size present in filters + if (!TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS.equals(af.getType())) { + evtFileSize = Stream.of(FileSizeFilter.values()) + .filter(filter -> af.getSize() >= filter.getMinBound() && (filter.getMaxBound() == null || af.getSize() < filter.getMaxBound())) + .findFirst() + .orElse(null); + } - if (fileExtensionDsMap.isEmpty() && mimeTypeDsMap.isEmpty() && fileSizeDsMap.isEmpty()) { - return Collections.emptySet(); - } - - clearRelevantCacheEntries(fileExtensionDsMap, mimeTypeDsMap, fileSizeDsMap); - - return getDAOEvents(fileExtensionDsMap, mimeTypeDsMap, fileSizeDsMap); - } - - /** - * - * Clears relevant cache entries from cache based on digest of autopsy - * events. - * - * @param fileExtensionDsMap Maps the file extension to the data sources - * where files were found with that extension. - * @param mimeTypeDsMap Maps the mime type to the data sources where - * files were found with that mime type. - * @param fileSizeDsMap Maps the size to the data sources where files - * - * @return The list of affected dao events. - */ - private Set getDAOEvents(Map> fileExtensionDsMap, - Map>> mimeTypeDsMap, - Map> fileSizeDsMap) { - - Stream fileExtStream = fileExtensionDsMap.entrySet().stream() - .flatMap(entry -> entry.getValue().stream().map(dsId -> new FileTypeExtensionsEvent(entry.getKey(), dsId))); - - Set fileMimeList = new HashSet<>(); - for (Entry>> prefixEntry : mimeTypeDsMap.entrySet()) { - String mimePrefix = prefixEntry.getKey(); - for (Entry> suffixEntry : prefixEntry.getValue().entrySet()) { - String mimeSuffix = suffixEntry.getKey(); - for (long dsId : suffixEntry.getValue()) { - String mimeType = mimePrefix + (mimeSuffix == null ? "" : ("/" + mimeSuffix)); - fileMimeList.add(new FileTypeMimeEvent(mimeType, dsId)); - } + if (evtExtFilters == null || evtExtFilters.isEmpty() && evtMimeType == null && evtFileSize == null) { + return Collections.emptySet(); } } - Stream fileSizeStream = fileSizeDsMap.entrySet().stream() - .flatMap(entry -> entry.getValue().stream().map(dsId -> new FileTypeSizeEvent(entry.getKey(), dsId))); + return invalidateAndReturnEvents(evtExtFilters, evtMimeType, evtFileSize, dsId, dataSourceAdded); + } - return Stream.of(fileExtStream, fileMimeList.stream(), fileSizeStream) - .flatMap(stream -> stream) + /** + * Handles invalidating caches and returning events based on digest. + * + * @param evtExtFilters The file extension filters or empty set. + * @param evtMimeType The mime type or null. + * @param evtFileSize The file size filter or null. + * @param dsId The data source id or null. + * @param dataSourceAdded Whether or not this is a data source added event. + * + * @return The set of dao events to be fired. + */ + private Set invalidateAndReturnEvents(Set evtExtFilters, String evtMimeType, + FileSizeFilter evtFileSize, Long dsId, boolean dataSourceAdded) { + + SubDAOUtils.invalidateKeys(this.searchParamsCache, + (Predicate) (searchParams) -> searchParamsMatchEvent(evtExtFilters, evtMimeType, + evtFileSize, dsId, dataSourceAdded, searchParams)); + + return getDAOEvents(evtExtFilters, evtMimeType, evtFileSize, dsId, dataSourceAdded); + } + + private boolean searchParamsMatchEvent(Set evtExtFilters, + String evtMimeType, + FileSizeFilter evtFileSize, + Long dsId, + boolean dataSourceAdded, + Object searchParams) { + + if (searchParams instanceof FileTypeExtensionsSearchParams) { + FileTypeExtensionsSearchParams extParams = (FileTypeExtensionsSearchParams) searchParams; + // if data source added or evtExtFilters contain param filter + return (dataSourceAdded || (evtExtFilters != null && evtExtFilters.contains(extParams.getFilter()))) + // and data source is either null or they are equal data source ids + && (extParams.getDataSourceId() == null || dsId == null || Objects.equals(extParams.getDataSourceId(), dsId)); + + } else if (searchParams instanceof FileTypeMimeSearchParams) { + FileTypeMimeSearchParams mimeParams = (FileTypeMimeSearchParams) searchParams; + return evtMimeType != null && evtMimeType.startsWith(mimeParams.getMimeType()) + && (mimeParams.getDataSourceId() == null || Objects.equals(mimeParams.getDataSourceId(), dsId)); + + } else if (searchParams instanceof FileTypeSizeSearchParams) { + FileTypeSizeSearchParams sizeParams = (FileTypeSizeSearchParams) searchParams; + // if data source added or size filter is equal to param filter + return (dataSourceAdded || Objects.equals(sizeParams.getSizeFilter(), evtFileSize)) + // and data source is either null or they are equal data source ids + && (sizeParams.getDataSourceId() == null || dsId == null || Objects.equals(sizeParams.getDataSourceId(), dsId)); + } else { + return false; + } + } + + /** + * Clears relevant cache entries from cache based on digest of autopsy + * events. + * + * @param extFilters The set of affected extension filters. + * @param mimeType The affected mime type or null. + * @param sizeFilter The affected size filter or null. + * @param dsId The file object id. + * @param dataSourceAdded A data source was added. + * + * @return The list of affected dao events. + */ + private Set getDAOEvents(Set extFilters, String mimeType, FileSizeFilter sizeFilter, Long dsId, boolean dataSourceAdded) { + + List daoEvents = extFilters == null + ? new ArrayList<>() + : extFilters.stream() + .map(extFilter -> new FileTypeExtensionsEvent(extFilter, dsId)) + .collect(Collectors.toList()); + + if (mimeType != null) { + daoEvents.add(new FileTypeMimeEvent(mimeType, dsId)); + } + + if (sizeFilter != null) { + daoEvents.add(new FileTypeSizeEvent(sizeFilter, dsId)); + } + + List treeEvents = this.treeCounts.enqueueAll(daoEvents).stream() + .map(daoEvt -> new TreeEvent(createTreeItem(daoEvt, TreeDisplayCount.INDETERMINATE), false)) + .collect(Collectors.toList()); + + // data source added events are not necessarily fired before ingest completed/cancelled, so don't handle dataSourceAdded events with delay. + Set forceRefreshEvents = (dataSourceAdded) + ? getFileViewRefreshEvents(dsId) + : Collections.emptySet(); + + List forceRefreshTreeEvents = forceRefreshEvents.stream() + .map(evt -> new TreeEvent(createTreeItem(evt, TreeDisplayCount.UNSPECIFIED), true)) + .collect(Collectors.toList()); + + return Stream.of(daoEvents, treeEvents, forceRefreshEvents, forceRefreshTreeEvents) + .flatMap(lst -> lst.stream()) .collect(Collectors.toSet()); } /** - * Clears relevant cache entries from cache based on digest of autopsy - * events. + * Returns events for when a full refresh is required because module content + * events will not necessarily provide events for files (i.e. data source + * added, ingest cancelled/completed). * - * @param fileExtensionDsMap Maps the file extension to the data sources - * where files were found with that extension. - * @param mimeTypeDsMap Maps the mime type to the data sources where - * files were found with that mime type. - * @param fileSizeDsMap Maps the size to the data sources where files - * were found within that size filter. + * @param dataSourceId The data source id or null if not applicable. + * + * @return The set of events that apply in this situation. */ - private void clearRelevantCacheEntries(Map> fileExtensionDsMap, - Map>> mimeTypeDsMap, - Map> fileSizeDsMap) { - - // invalidate cache entries that are affected by events - ConcurrentMap, SearchResultsDTO> concurrentMap = this.searchParamsCache.asMap(); - concurrentMap.forEach((k, v) -> { - Object baseParams = k.getParamData(); - if (baseParams instanceof FileTypeExtensionsSearchParams) { - FileTypeExtensionsSearchParams extParams = (FileTypeExtensionsSearchParams) baseParams; - // if search params have a filter where extension is present and the data source id is null or == - boolean isMatch = extParams.getFilter().getFilter().stream().anyMatch((ext) -> { - Set dsIds = fileExtensionDsMap.get(ext); - return (dsIds != null && (extParams.getDataSourceId() == null || dsIds.contains(extParams.getDataSourceId()))); - }); - - if (isMatch) { - concurrentMap.remove(k); - } - } else if (baseParams instanceof FileTypeMimeSearchParams) { - FileTypeMimeSearchParams mimeParams = (FileTypeMimeSearchParams) baseParams; - Pair mimePieces = getMimePieces(mimeParams.getMimeType()); - Map> suffixes = mimeTypeDsMap.get(mimePieces.getKey()); - if (suffixes == null) { - return; - } - - // if search params is top level mime prefix (without suffix) and data source is null or ==. - if (mimePieces.getValue() == null - && (mimeParams.getDataSourceId() == null - || suffixes.values().stream().flatMap(set -> set.stream()).anyMatch(ds -> Objects.equals(mimeParams.getDataSourceId(), ds)))) { - - concurrentMap.remove(k); - // otherwise, see if suffix is present - } else { - Set dataSources = suffixes.get(mimePieces.getValue()); - if (dataSources != null && (mimeParams.getDataSourceId() == null || dataSources.contains(mimeParams.getDataSourceId()))) { - concurrentMap.remove(k); - } - } - - } else if (baseParams instanceof FileTypeSizeSearchParams) { - FileTypeSizeSearchParams sizeParams = (FileTypeSizeSearchParams) baseParams; - Set dataSources = fileSizeDsMap.get(sizeParams.getSizeFilter()); - if (dataSources != null && (sizeParams.getDataSourceId() == null || dataSources.contains(sizeParams.getDataSourceId()))) { - concurrentMap.remove(k); - } - } - }); + private Set getFileViewRefreshEvents(Long dataSourceId) { + return ImmutableSet.of( + new FileTypeSizeEvent(null, dataSourceId), + new FileTypeExtensionsEvent(null, dataSourceId) + ); } /** diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/events/FileSystemContentEvent.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/events/FileSystemContentEvent.java index a72c93cea2..8efc3d5ef9 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/events/FileSystemContentEvent.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/events/FileSystemContentEvent.java @@ -19,6 +19,8 @@ package org.sleuthkit.autopsy.mainui.datamodel.events; import java.util.Objects; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.Host; /** * An event signaling that children files were added or removed from the given @@ -26,26 +28,40 @@ import java.util.Objects; */ public class FileSystemContentEvent implements DAOEvent { - private final Long contentObjectId; + private final Content content; + private final Long parentObjId; + private final Host parentHost; + + public FileSystemContentEvent(Content content, Long parentObjId, Host parentHost) { + this.content = content; + this.parentObjId = parentObjId; + this.parentHost = parentHost; + } + + public Long getParentObjId() { + return parentObjId; + } + + public Host getParentHost() { + return parentHost; + } /** - * Main constructor. - * - * @param contentObjectId The parent content object id. If null, performs - * full refresh of file tree. + * @return The content associated with the event, if null, triggers a full + * refresh. */ - public FileSystemContentEvent(Long contentObjectId) { - this.contentObjectId = contentObjectId; + public Content getContent() { + return content; } public Long getContentObjectId() { - return contentObjectId; + return (content == null) ? null : content.getId(); } @Override public int hashCode() { int hash = 7; - hash = 67 * hash + Objects.hashCode(this.contentObjectId); + hash = 71 * hash + Objects.hashCode(this.content); return hash; } @@ -61,7 +77,7 @@ public class FileSystemContentEvent implements DAOEvent { return false; } final FileSystemContentEvent other = (FileSystemContentEvent) obj; - if (!Objects.equals(this.contentObjectId, other.contentObjectId)) { + if (!Objects.equals(this.content, other.content)) { return false; } return true; diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/events/FileTypeExtensionsEvent.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/events/FileTypeExtensionsEvent.java index dadac922b1..354cb25010 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/events/FileTypeExtensionsEvent.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/events/FileTypeExtensionsEvent.java @@ -19,34 +19,42 @@ package org.sleuthkit.autopsy.mainui.datamodel.events; import java.util.Objects; +import org.sleuthkit.autopsy.mainui.datamodel.FileExtSearchFilter; /** - * An event to signal that files have been added or removed - * with the given extension on the given data source. + * An event to signal that files have been added or removed with the given + * extension on the given data source. */ public class FileTypeExtensionsEvent implements DAOEvent { - private final String extension; - private final long dataSourceId; + private final FileExtSearchFilter extensionFilter; + private final Long dataSourceId; - public FileTypeExtensionsEvent(String extension, long dataSourceId) { - this.extension = extension; + /** + * Main constructor. + * + * @param extensionFilter The extension filter. If null, indicates full + * refresh necessary. + * @param dataSourceId The data source id. + */ + public FileTypeExtensionsEvent(FileExtSearchFilter extensionFilter, Long dataSourceId) { + this.extensionFilter = extensionFilter; this.dataSourceId = dataSourceId; } - public String getExtension() { - return extension; + public FileExtSearchFilter getExtensionFilter() { + return extensionFilter; } - public long getDataSourceId() { + public Long getDataSourceId() { return dataSourceId; } @Override public int hashCode() { - int hash = 7; - hash = 59 * hash + Objects.hashCode(this.extension); - hash = 59 * hash + (int) (this.dataSourceId ^ (this.dataSourceId >>> 32)); + int hash = 3; + hash = 89 * hash + Objects.hashCode(this.extensionFilter); + hash = 89 * hash + Objects.hashCode(this.dataSourceId); return hash; } @@ -62,10 +70,10 @@ public class FileTypeExtensionsEvent implements DAOEvent { return false; } final FileTypeExtensionsEvent other = (FileTypeExtensionsEvent) obj; - if (this.dataSourceId != other.dataSourceId) { + if (!Objects.equals(this.extensionFilter, other.extensionFilter)) { return false; } - if (!Objects.equals(this.extension, other.extension)) { + if (!Objects.equals(this.dataSourceId, other.dataSourceId)) { return false; } return true; diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/events/FileTypeSizeEvent.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/events/FileTypeSizeEvent.java index eb0f37f8a4..1fe4256130 100755 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/events/FileTypeSizeEvent.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/events/FileTypeSizeEvent.java @@ -22,14 +22,21 @@ import java.util.Objects; import org.sleuthkit.autopsy.mainui.datamodel.FileSizeFilter; /** - * An event to signal that files have been added or removed - * within the given size range on the given data source. + * An event to signal that files have been added or removed within the given + * size range on the given data source. */ public class FileTypeSizeEvent implements DAOEvent { private final FileSizeFilter sizeFilter; private final Long dataSourceId; + /** + * Main constructor. + * + * @param sizeFilter The size filter. If null, indicates full refresh is + * necessary. + * @param dataSourceId The data source id or null. + */ public FileTypeSizeEvent(FileSizeFilter sizeFilter, Long dataSourceId) { this.sizeFilter = sizeFilter; this.dataSourceId = dataSourceId; @@ -45,9 +52,9 @@ public class FileTypeSizeEvent implements DAOEvent { @Override public int hashCode() { - int hash = 7; - hash = 53 * hash + Objects.hashCode(this.sizeFilter); - hash = 53 * hash + Objects.hashCode(this.dataSourceId); + int hash = 5; + hash = 73 * hash + Objects.hashCode(this.sizeFilter); + hash = 73 * hash + Objects.hashCode(this.dataSourceId); return hash; } @@ -72,6 +79,8 @@ public class FileTypeSizeEvent implements DAOEvent { return true; } + + @Override public Type getType() { return Type.RESULT; diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/events/KeywordHitEvent.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/events/KeywordHitEvent.java index 6141ac7805..82e53abc79 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/events/KeywordHitEvent.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/events/KeywordHitEvent.java @@ -18,7 +18,9 @@ */ package org.sleuthkit.autopsy.mainui.datamodel.events; +import java.util.Objects; import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.TskData; /** * An event for an artifact added or changed of a particular type possibly for a @@ -26,20 +28,71 @@ import org.sleuthkit.datamodel.BlackboardArtifact; */ public class KeywordHitEvent extends AnalysisResultSetEvent { - private final String regex; + private final String searchString; private final String match; + private final TskData.KeywordSearchQueryType searchType; - public KeywordHitEvent(String regex, String match, String setName, BlackboardArtifact.Type artifactType, long dataSourceId) { + /** + * Main constructor. + * + * @param searchString The search string or regex. + * @param match The match string. + * @param searchType THe search type. + * @param setName The set name. + * @param artifactType The artifact type. + * @param dataSourceId The data source id. + */ + public KeywordHitEvent(String searchString, String match, TskData.KeywordSearchQueryType searchType, String setName, BlackboardArtifact.Type artifactType, long dataSourceId) { super(setName, artifactType, dataSourceId); - this.regex = regex; + this.searchString = searchString; this.match = match; + this.searchType = searchType; } - public String getRegex() { - return regex; + public String getSearchString() { + return searchString; } public String getMatch() { return match; } + + public TskData.KeywordSearchQueryType getSearchType() { + return searchType; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 67 * hash + Objects.hashCode(this.searchString); + hash = 67 * hash + Objects.hashCode(this.match); + hash = 67 * hash + Objects.hashCode(this.searchType); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final KeywordHitEvent other = (KeywordHitEvent) obj; + if (!Objects.equals(this.searchString, other.searchString)) { + return false; + } + if (!Objects.equals(this.match, other.match)) { + return false; + } + if (this.searchType != other.searchType) { + return false; + } + return true; + } + + } diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/events/TagsEvent.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/events/TagsEvent.java index 948f96e20d..802303b744 100755 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/events/TagsEvent.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/events/TagsEvent.java @@ -20,6 +20,7 @@ package org.sleuthkit.autopsy.mainui.datamodel.events; import java.util.Objects; import org.sleuthkit.autopsy.mainui.datamodel.TagsSearchParams.TagType; +import org.sleuthkit.datamodel.TagName; /** * An event to signal that tags have been added or removed on the @@ -28,21 +29,22 @@ import org.sleuthkit.autopsy.mainui.datamodel.TagsSearchParams.TagType; public class TagsEvent implements DAOEvent { private final TagType type; - private final Long tagNameId; + private final TagName tagName; private final Long dataSourceId; - public TagsEvent(TagType type, Long tagNameId, Long dataSourceId) { + public TagsEvent(TagType type, TagName tagName, Long dataSourceId) { this.type = type; - this.tagNameId = tagNameId; + this.tagName = tagName; this.dataSourceId = dataSourceId; } + public TagType getTagType() { return type; } - public Long getTagNameId() { - return tagNameId; + public TagName getTagName() { + return tagName; } /** @@ -57,7 +59,7 @@ public class TagsEvent implements DAOEvent { public int hashCode() { int hash = 7; hash = 97 * hash + Objects.hashCode(this.type); - hash = 97 * hash + Objects.hashCode(this.tagNameId); + hash = 97 * hash + Objects.hashCode(this.tagName); hash = 97 * hash + Objects.hashCode(this.dataSourceId); return hash; } @@ -77,7 +79,7 @@ public class TagsEvent implements DAOEvent { if (this.type != other.type) { return false; } - if (!Objects.equals(this.tagNameId, other.tagNameId)) { + if (!Objects.equals(this.tagName, other.tagName)) { return false; } if (!Objects.equals(this.dataSourceId, other.dataSourceId)) { @@ -86,6 +88,8 @@ public class TagsEvent implements DAOEvent { return true; } + + @Override public Type getType() { return Type.RESULT; diff --git a/Core/src/org/sleuthkit/autopsy/mainui/nodes/AnalysisResultTypeFactory.java b/Core/src/org/sleuthkit/autopsy/mainui/nodes/AnalysisResultTypeFactory.java index 3aa8a16c91..9473354e6e 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/nodes/AnalysisResultTypeFactory.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/nodes/AnalysisResultTypeFactory.java @@ -22,6 +22,7 @@ import org.sleuthkit.autopsy.mainui.datamodel.KeywordSearchTermParams; import org.sleuthkit.autopsy.mainui.datamodel.KeywordMatchParams; import com.google.common.collect.ImmutableSet; import java.util.Comparator; +import java.util.Objects; import java.util.Set; import java.util.concurrent.ExecutionException; import org.openide.nodes.ChildFactory; @@ -29,11 +30,13 @@ import org.openide.nodes.Children; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.corecomponents.DataResultTopComponent; import org.sleuthkit.autopsy.datamodel.utils.IconsUtil; +import org.sleuthkit.autopsy.mainui.datamodel.AnalysisResultDAO; import org.sleuthkit.autopsy.mainui.datamodel.AnalysisResultSearchParam; import org.sleuthkit.autopsy.mainui.datamodel.AnalysisResultSetSearchParam; import org.sleuthkit.autopsy.mainui.datamodel.KeywordHitSearchParam; import org.sleuthkit.autopsy.mainui.datamodel.MainDAO; import org.sleuthkit.autopsy.mainui.datamodel.TreeResultsDTO; +import org.sleuthkit.autopsy.mainui.datamodel.TreeResultsDTO.TreeItemDTO; import org.sleuthkit.autopsy.mainui.datamodel.events.TreeEvent; import static org.sleuthkit.autopsy.mainui.nodes.TreeNode.getDefaultLookup; import org.sleuthkit.datamodel.BlackboardArtifact; @@ -50,7 +53,8 @@ public class AnalysisResultTypeFactory extends TreeChildFactory SET_TREE_ARTIFACTS = ImmutableSet.of( BlackboardArtifact.Type.TSK_HASHSET_HIT.getTypeID(), BlackboardArtifact.Type.TSK_INTERESTING_ARTIFACT_HIT.getTypeID(), - BlackboardArtifact.Type.TSK_INTERESTING_FILE_HIT.getTypeID() + BlackboardArtifact.Type.TSK_INTERESTING_FILE_HIT.getTypeID(), + BlackboardArtifact.Type.TSK_INTERESTING_ITEM.getTypeID() ); /** @@ -93,14 +97,29 @@ public class AnalysisResultTypeFactory extends TreeChildFactory getOrCreateRelevantChild(TreeEvent daoEvt) { - // GVDTODO + protected TreeResultsDTO.TreeItemDTO getOrCreateRelevantChild(TreeEvent treeEvt) { + + TreeResultsDTO.TreeItemDTO originalTreeItem = super.getTypedTreeItem(treeEvt, AnalysisResultSearchParam.class); + + if (originalTreeItem != null + && !AnalysisResultDAO.getIgnoredTreeTypes().contains(originalTreeItem.getSearchParams().getArtifactType()) + && (this.dataSourceId == null || Objects.equals(this.dataSourceId, originalTreeItem.getSearchParams().getDataSourceId()))) { + + // generate new type so that if it is a subtree event (i.e. keyword hits), the right tree item is created. + AnalysisResultSearchParam searchParam = originalTreeItem.getSearchParams(); + return new TreeResultsDTO.TreeItemDTO<>( + AnalysisResultSearchParam.getTypeId(), + new AnalysisResultSearchParam(searchParam.getArtifactType(), this.dataSourceId), + searchParam.getArtifactType().getTypeID(), + searchParam.getArtifactType().getDisplayName(), + originalTreeItem.getDisplayCount()); + } return null; } @Override - public int compare(AnalysisResultSearchParam o1, AnalysisResultSearchParam o2) { - return o1.getArtifactType().getDisplayName().compareTo(o2.getArtifactType().getDisplayName()); + public int compare(TreeItemDTO o1, TreeItemDTO o2) { + return o1.getSearchParams().getArtifactType().getDisplayName().compareTo(o2.getSearchParams().getArtifactType().getDisplayName()); } /** @@ -180,14 +199,28 @@ public class AnalysisResultTypeFactory extends TreeChildFactory getOrCreateRelevantChild(TreeEvent daoEvt) { - // GVDTODO + protected TreeResultsDTO.TreeItemDTO getOrCreateRelevantChild(TreeEvent treeEvt) { + TreeResultsDTO.TreeItemDTO originalTreeItem = super.getTypedTreeItem(treeEvt, AnalysisResultSetSearchParam.class); + + if (originalTreeItem != null + && originalTreeItem.getSearchParams().getArtifactType().equals(this.artifactType) + && (this.dataSourceId == null || Objects.equals(this.dataSourceId, originalTreeItem.getSearchParams().getDataSourceId()))) { + + // generate new type so that if it is a subtree event (i.e. keyword hits), the right tree item is created. + AnalysisResultSetSearchParam searchParam = originalTreeItem.getSearchParams(); + return new TreeResultsDTO.TreeItemDTO<>( + AnalysisResultSetSearchParam.getTypeId(), + new AnalysisResultSetSearchParam(this.artifactType, this.dataSourceId, searchParam.getSetName()), + searchParam.getSetName(), + searchParam.getSetName() == null ? nullSetName : searchParam.getSetName(), + originalTreeItem.getDisplayCount()); + } return null; } @Override - public int compare(AnalysisResultSetSearchParam o1, AnalysisResultSetSearchParam o2) { - return STRING_COMPARATOR.compare(o1.getSetName(), o2.getSetName()); + public int compare(TreeItemDTO o1, TreeItemDTO o2) { + return STRING_COMPARATOR.compare(o1.getSearchParams().getSetName(), o2.getSearchParams().getSetName()); } } @@ -277,14 +310,38 @@ public class AnalysisResultTypeFactory extends TreeChildFactory getOrCreateRelevantChild(TreeEvent daoEvt) { - // GVDTODO + protected TreeResultsDTO.TreeItemDTO getOrCreateRelevantChild(TreeEvent treeEvt) { + TreeResultsDTO.TreeItemDTO originalTreeItem = super.getTypedTreeItem(treeEvt, KeywordSearchTermParams.class); + + if (originalTreeItem != null + && Objects.equals(originalTreeItem.getSearchParams().getSetName(), this.setParams.getSetName()) + && (this.setParams.getDataSourceId() == null + || Objects.equals(this.setParams.getDataSourceId(), originalTreeItem.getSearchParams().getDataSourceId()))) { + + KeywordSearchTermParams searchParam = originalTreeItem.getSearchParams(); + String searchTermDisplayName = MainDAO.getInstance().getAnalysisResultDAO() + .getSearchTermDisplayName(searchParam.getSearchTerm(), searchParam.getSearchType()); + + return new TreeResultsDTO.TreeItemDTO<>( + KeywordSearchTermParams.getTypeId(), + new KeywordSearchTermParams( + this.setParams.getSetName(), + searchParam.getSearchTerm(), + searchParam.getSearchType(), + searchParam.hasChildren(), + this.setParams.getDataSourceId() + ), + searchTermDisplayName, + searchTermDisplayName, + originalTreeItem.getDisplayCount() + ); + } return null; } @Override - public int compare(KeywordSearchTermParams o1, KeywordSearchTermParams o2) { - return STRING_COMPARATOR.compare(o1.getSearchTerm(), o2.getSearchTerm()); + public int compare(TreeItemDTO o1, TreeItemDTO o2) { + return STRING_COMPARATOR.compare(o1.getSearchParams().getSearchTerm(), o2.getSearchParams().getSearchTerm()); } } @@ -360,14 +417,36 @@ public class AnalysisResultTypeFactory extends TreeChildFactory getOrCreateRelevantChild(TreeEvent daoEvt) { - // GVDTODO + protected TreeResultsDTO.TreeItemDTO getOrCreateRelevantChild(TreeEvent treeEvt) { + TreeResultsDTO.TreeItemDTO originalTreeItem = super.getTypedTreeItem(treeEvt, KeywordMatchParams.class); + + if (originalTreeItem != null + && Objects.equals(originalTreeItem.getSearchParams().getSetName(), this.setParams.getSetName()) + && (this.setParams.getDataSourceId() == null + || Objects.equals(this.setParams.getDataSourceId(), originalTreeItem.getSearchParams().getDataSourceId()))) { + + // generate new type so that if it is a subtree event (i.e. keyword hits), the right tree item is created. + KeywordMatchParams searchParam = originalTreeItem.getSearchParams(); + return new TreeResultsDTO.TreeItemDTO<>( + KeywordMatchParams.getTypeId(), + new KeywordMatchParams( + this.setParams.getSetName(), + this.setParams.getSearchTerm(), + searchParam.getKeywordMatch(), + this.setParams.getSearchType(), + this.setParams.getDataSourceId() + ), + searchParam.getKeywordMatch(), + searchParam.getKeywordMatch() == null ? "" : searchParam.getKeywordMatch(), + originalTreeItem.getDisplayCount() + ); + } return null; } @Override - public int compare(KeywordMatchParams o1, KeywordMatchParams o2) { - return STRING_COMPARATOR.compare(o1.getKeywordMatch(), o2.getKeywordMatch()); + public int compare(TreeItemDTO o1, TreeItemDTO o2) { + return STRING_COMPARATOR.compare(o1.getSearchParams().getKeywordMatch(), o2.getSearchParams().getKeywordMatch()); } } diff --git a/Core/src/org/sleuthkit/autopsy/mainui/nodes/DataArtifactTypeFactory.java b/Core/src/org/sleuthkit/autopsy/mainui/nodes/DataArtifactTypeFactory.java index 511cc8486c..b5cbdfe858 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/nodes/DataArtifactTypeFactory.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/nodes/DataArtifactTypeFactory.java @@ -77,7 +77,7 @@ public class DataArtifactTypeFactory extends TreeChildFactory( - BlackboardArtifact.Category.DATA_ARTIFACT.name(), + DataArtifactSearchParam.getTypeId(), new DataArtifactSearchParam(searchParam.getArtifactType(), this.dataSourceId), searchParam.getArtifactType().getTypeID(), MainDAO.getInstance().getDataArtifactsDAO().getDisplayName(searchParam.getArtifactType()), @@ -87,9 +87,9 @@ public class DataArtifactTypeFactory extends TreeChildFactory o1, TreeItemDTO o2) { DataArtifactDAO dao = MainDAO.getInstance().getDataArtifactsDAO(); - return dao.getDisplayName(o1.getArtifactType()).compareToIgnoreCase(dao.getDisplayName(o2.getArtifactType())); + return dao.getDisplayName(o1.getSearchParams().getArtifactType()).compareToIgnoreCase(dao.getDisplayName(o2.getSearchParams().getArtifactType())); } private static String getIconPath(BlackboardArtifact.Type artType) { @@ -206,8 +206,8 @@ public class DataArtifactTypeFactory extends TreeChildFactory o1, TreeItemDTO o2) { + return o1.getSearchParams().getType().getDisplayName().compareToIgnoreCase(o2.getSearchParams().getType().getDisplayName()); } } diff --git a/Core/src/org/sleuthkit/autopsy/mainui/nodes/FileSystemFactory.java b/Core/src/org/sleuthkit/autopsy/mainui/nodes/FileSystemFactory.java index 9289e56ffc..f8ab4f652d 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/nodes/FileSystemFactory.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/nodes/FileSystemFactory.java @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.mainui.nodes; +import java.util.Objects; import java.util.Optional; import org.openide.nodes.Children; import org.openide.nodes.Node; @@ -35,9 +36,12 @@ import org.sleuthkit.autopsy.directorytree.ExtractUnallocAction; import org.sleuthkit.autopsy.directorytree.FileSystemDetailsAction; import org.sleuthkit.autopsy.mainui.datamodel.FileSystemContentSearchParam; import org.sleuthkit.autopsy.mainui.datamodel.FileSystemColumnUtils; +import org.sleuthkit.autopsy.mainui.datamodel.FileSystemDAO.FileSystemTreeEvent; +import org.sleuthkit.autopsy.mainui.datamodel.FileSystemDAO.FileSystemTreeItem; import org.sleuthkit.autopsy.mainui.datamodel.MediaTypeUtils; import org.sleuthkit.autopsy.mainui.datamodel.MainDAO; import org.sleuthkit.autopsy.mainui.datamodel.TreeResultsDTO; +import org.sleuthkit.autopsy.mainui.datamodel.TreeResultsDTO.TreeItemDTO; import org.sleuthkit.autopsy.mainui.datamodel.events.TreeEvent; import static org.sleuthkit.autopsy.mainui.nodes.NodeIconUtil.CARVED_FILE; import static org.sleuthkit.autopsy.mainui.nodes.NodeIconUtil.DELETED_FILE; @@ -140,14 +144,31 @@ public class FileSystemFactory extends TreeChildFactory getOrCreateRelevantChild(TreeEvent treeEvt) { - // GVDTODO + if (treeEvt instanceof FileSystemTreeEvent) { + FileSystemTreeEvent fsTreeEvent = (FileSystemTreeEvent) treeEvt; + // when getContentObjectId == null, trigger refresh, otherwise, see if common parent + if (fsTreeEvent.getItemRecord().getSearchParams().getContentObjectId() == null + || (Objects.equals(this.host, fsTreeEvent.getParentHost()) + && Objects.equals(this.contentId, fsTreeEvent.getParentContentId()))) { + + return fsTreeEvent.getItemRecord(); + } + } + return null; } @Override - public int compare(FileSystemContentSearchParam o1, FileSystemContentSearchParam o2) { - // GVDTODO - return 0; + public int compare(TreeItemDTO o1, TreeItemDTO o2) { + if (o1 instanceof FileSystemTreeItem && o2 instanceof FileSystemTreeItem) { + FileSystemTreeItem fs1 = (FileSystemTreeItem) o1; + FileSystemTreeItem fs2 = (FileSystemTreeItem) o2; + // ordering taken from SELECT_FILES_BY_PARENT in SleuthkitCase + if (fs1.getMetaType().getValue() != fs2.getMetaType().getValue()) { + return -Short.compare(fs1.getMetaType().getValue(), fs2.getMetaType().getValue()); + } + } + return o1.getDisplayName().compareToIgnoreCase(o2.getDisplayName()); } /** @@ -196,14 +217,23 @@ public class FileSystemFactory extends TreeChildFactory getOrCreateRelevantChild(TreeEvent treeEvt) { - // GVDTODO + if (treeEvt instanceof FileSystemTreeEvent) { + FileSystemTreeEvent fsTreeEvent = (FileSystemTreeEvent) treeEvt; + // when getContentObjectId == null, trigger refresh, otherwise, see if common parent + if (fsTreeEvent.getItemRecord().getSearchParams().getContentObjectId() == null + || Objects.equals(fsTreeEvent.getItemRecord().getSearchParams().getContentObjectId(), dataSourceId)) { + + return fsTreeEvent.getItemRecord(); + } + } + return null; + } @Override - public int compare(FileSystemContentSearchParam o1, FileSystemContentSearchParam o2) { - // GVDTODO - return 0; + public int compare(TreeItemDTO o1, TreeItemDTO o2) { + return o1.getDisplayName().compareToIgnoreCase(o2.getDisplayName()); } } diff --git a/Core/src/org/sleuthkit/autopsy/mainui/nodes/TreeChildFactory.java b/Core/src/org/sleuthkit/autopsy/mainui/nodes/TreeChildFactory.java index 74baf01e6f..0c339674fe 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/nodes/TreeChildFactory.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/nodes/TreeChildFactory.java @@ -42,27 +42,13 @@ import org.sleuthkit.autopsy.mainui.datamodel.events.TreeEvent; /** * Factory for populating child nodes in a tree based on TreeResultsDTO */ -public abstract class TreeChildFactory extends ChildFactory.Detachable implements Comparator { +public abstract class TreeChildFactory extends ChildFactory.Detachable implements Comparator> { private static final Logger logger = Logger.getLogger(TreeChildFactory.class.getName()); private final PropertyChangeListener pcl = (PropertyChangeEvent evt) -> { if (evt.getNewValue() instanceof DAOAggregateEvent) { - DAOAggregateEvent aggEvt = (DAOAggregateEvent) evt.getNewValue(); - for (DAOEvent daoEvt : aggEvt.getEvents()) { - if (daoEvt instanceof TreeEvent) { - TreeEvent treeEvt = (TreeEvent) daoEvt; - TreeItemDTO item = getOrCreateRelevantChild(treeEvt); - if (item != null) { - if (treeEvt.isRefreshRequired()) { - update(); - break; - } else { - updateNodeData(item); - } - } - } - } + handleDAOAggregateEvent((DAOAggregateEvent) evt.getNewValue()); } }; @@ -83,20 +69,45 @@ public abstract class TreeChildFactory extends ChildFactory.Detachable> idMapping = new HashMap<>(); + /** + * Handles processing and updating due to an aggregate event. This method + * can be overridden for custom behavior while handling DAO aggregate + * events. + * + * @param aggEvt The aggregate event. + */ + protected void handleDAOAggregateEvent(DAOAggregateEvent aggEvt) { + for (DAOEvent daoEvt : aggEvt.getEvents()) { + if (daoEvt instanceof TreeEvent) { + TreeEvent treeEvt = (TreeEvent) daoEvt; + TreeItemDTO item = getOrCreateRelevantChild(treeEvt); + if (item != null) { + if (treeEvt.isRefreshRequired()) { + update(); + break; + } else { + updateNodeData(item); + } + } + } + } + } + @Override protected boolean createKeys(List toPopulate) { List> itemsList; - synchronized (resultsUpdateLock) { - // Load data from DAO if we haven't already - if (curResults == null) { - try { - updateData(); - } catch (IllegalArgumentException | ExecutionException ex) { - logger.log(Level.WARNING, "An error occurred while fetching keys", ex); - return false; - } + + // Load data from DAO if we haven't already + if (curResults == null) { + try { + updateData(); + } catch (IllegalArgumentException | ExecutionException ex) { + logger.log(Level.WARNING, "An error occurred while fetching keys", ex); + return false; } - // make copy to avoid concurrent modification + } + // make copy to avoid concurrent modification + synchronized (resultsUpdateLock) { itemsList = new ArrayList<>(curItemsList); } @@ -140,7 +151,8 @@ public abstract class TreeChildFactory extends ChildFactory.Detachable curItem = this.curItemsList.get(insertIndex); + if (this.compare(item, curItem) < 0) { break; } } @@ -159,8 +171,9 @@ public abstract class TreeChildFactory extends ChildFactory.Detachable newResults = getChildResults(); synchronized (resultsUpdateLock) { - this.curResults = getChildResults(); + this.curResults = newResults; Map> idMapping = new HashMap<>(); List> curItemsList = new ArrayList<>(); for (TreeItemDTO item : this.curResults.getItems()) { diff --git a/Core/src/org/sleuthkit/autopsy/mainui/nodes/ViewsTypeFactory.java b/Core/src/org/sleuthkit/autopsy/mainui/nodes/ViewsTypeFactory.java index c4ddbf366e..373a520088 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/nodes/ViewsTypeFactory.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/nodes/ViewsTypeFactory.java @@ -18,15 +18,15 @@ */ package org.sleuthkit.autopsy.mainui.nodes; -import java.beans.PropertyChangeEvent; import java.util.Collection; import java.util.Comparator; +import java.util.Objects; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import java.util.stream.Stream; import org.openide.nodes.Children; import org.sleuthkit.autopsy.corecomponents.DataResultTopComponent; -import org.sleuthkit.autopsy.ingest.ModuleContentEvent; +import org.sleuthkit.autopsy.mainui.datamodel.AnalysisResultSearchParam; import org.sleuthkit.autopsy.mainui.datamodel.FileExtDocumentFilter; import org.sleuthkit.autopsy.mainui.datamodel.FileExtExecutableFilter; import org.sleuthkit.autopsy.mainui.datamodel.FileExtRootFilter; @@ -36,8 +36,10 @@ import org.sleuthkit.autopsy.mainui.datamodel.FileTypeMimeSearchParams; import org.sleuthkit.autopsy.mainui.datamodel.FileTypeSizeSearchParams; import org.sleuthkit.autopsy.mainui.datamodel.MainDAO; import org.sleuthkit.autopsy.mainui.datamodel.TreeResultsDTO; +import org.sleuthkit.autopsy.mainui.datamodel.TreeResultsDTO.TreeItemDTO; +import org.sleuthkit.autopsy.mainui.datamodel.events.DAOAggregateEvent; +import org.sleuthkit.autopsy.mainui.datamodel.events.DAOEvent; import org.sleuthkit.autopsy.mainui.datamodel.events.TreeEvent; -import org.sleuthkit.datamodel.AbstractFile; /** * @@ -74,14 +76,47 @@ public class ViewsTypeFactory { } @Override - protected TreeResultsDTO.TreeItemDTO getOrCreateRelevantChild(TreeEvent daoEvt) { - // GVDTODO + protected void handleDAOAggregateEvent(DAOAggregateEvent aggEvt) { + for (DAOEvent evt : aggEvt.getEvents()) { + if (evt instanceof TreeEvent) { + TreeResultsDTO.TreeItemDTO treeItem = super.getTypedTreeItem((TreeEvent) evt, FileTypeSizeSearchParams.class); + // if file type size search params has null filter, trigger full refresh + if (treeItem != null && treeItem.getSearchParams().getSizeFilter() == null) { + super.update(); + return; + } + } + } + + super.handleDAOAggregateEvent(aggEvt); + } + + @Override + protected TreeResultsDTO.TreeItemDTO getOrCreateRelevantChild(TreeEvent treeEvt) { + TreeResultsDTO.TreeItemDTO originalTreeItem = super.getTypedTreeItem(treeEvt, FileTypeSizeSearchParams.class); + + if (originalTreeItem != null + // only create child if size filter is present (if null, update should be triggered separately) + && originalTreeItem.getSearchParams().getSizeFilter() != null + && (this.dataSourceId == null + || originalTreeItem.getSearchParams().getDataSourceId() == null + || Objects.equals(this.dataSourceId, originalTreeItem.getSearchParams().getDataSourceId()))) { + + // generate new type so that if it is a subtree event (i.e. keyword hits), the right tree item is created. + FileTypeSizeSearchParams searchParam = originalTreeItem.getSearchParams(); + return new TreeResultsDTO.TreeItemDTO<>( + AnalysisResultSearchParam.getTypeId(), + new FileTypeSizeSearchParams(searchParam.getSizeFilter(), this.dataSourceId), + searchParam.getSizeFilter(), + searchParam.getSizeFilter().getDisplayName(), + originalTreeItem.getDisplayCount()); + } return null; } @Override - public int compare(FileTypeSizeSearchParams o1, FileTypeSizeSearchParams o2) { - return Integer.compare(o1.getSizeFilter().getId(), o2.getSizeFilter().getId()); + public int compare(TreeItemDTO o1, TreeItemDTO o2) { + return Integer.compare(o1.getSearchParams().getSizeFilter().getId(), o2.getSearchParams().getSizeFilter().getId()); } /** @@ -133,14 +168,34 @@ public class ViewsTypeFactory { } @Override - protected TreeResultsDTO.TreeItemDTO getOrCreateRelevantChild(TreeEvent daoEvt) { - // GVDTODO + protected TreeResultsDTO.TreeItemDTO getOrCreateRelevantChild(TreeEvent treeEvt) { + TreeResultsDTO.TreeItemDTO originalTreeItem = super.getTypedTreeItem(treeEvt, FileTypeMimeSearchParams.class); + + if (originalTreeItem != null + && (this.dataSourceId == null || Objects.equals(this.dataSourceId, originalTreeItem.getSearchParams().getDataSourceId()))) { + + // generate new type so that if it is a subtree event (i.e. keyword hits), the right tree item is created. + FileTypeMimeSearchParams searchParam = originalTreeItem.getSearchParams(); + String mimePrefix = searchParam.getMimeType() == null ? "" : searchParam.getMimeType(); + int indexOfSlash = mimePrefix.indexOf("/"); + if (indexOfSlash >= 0) { + mimePrefix = mimePrefix.substring(0, indexOfSlash); + } + + return new TreeResultsDTO.TreeItemDTO<>( + AnalysisResultSearchParam.getTypeId(), + new FileTypeMimeSearchParams(mimePrefix, this.dataSourceId), + mimePrefix, + mimePrefix, + originalTreeItem.getDisplayCount()); + } return null; + } @Override - public int compare(FileTypeMimeSearchParams o1, FileTypeMimeSearchParams o2) { - return STRING_COMPARATOR.compare(o1.getMimeType(), o2.getMimeType()); + public int compare(TreeItemDTO o1, TreeItemDTO o2) { + return STRING_COMPARATOR.compare(o1.getSearchParams().getMimeType(), o2.getSearchParams().getMimeType()); } static class FileMimePrefixNode extends TreeNode { @@ -193,14 +248,30 @@ public class ViewsTypeFactory { } @Override - protected TreeResultsDTO.TreeItemDTO getOrCreateRelevantChild(TreeEvent daoEvt) { - // GVDTODO + protected TreeResultsDTO.TreeItemDTO getOrCreateRelevantChild(TreeEvent treeEvt) { + TreeResultsDTO.TreeItemDTO originalTreeItem = super.getTypedTreeItem(treeEvt, FileTypeMimeSearchParams.class); + + String prefixWithSlash = this.mimeTypePrefix + "/"; + if (originalTreeItem != null + && (originalTreeItem.getSearchParams().getMimeType().startsWith(prefixWithSlash)) + && (this.dataSourceId == null || Objects.equals(this.dataSourceId, originalTreeItem.getSearchParams().getDataSourceId()))) { + + // generate new type so that if it is a subtree event (i.e. keyword hits), the right tree item is created. + FileTypeMimeSearchParams searchParam = originalTreeItem.getSearchParams(); + String mimeSuffix = searchParam.getMimeType().substring(prefixWithSlash.length()); + return new TreeResultsDTO.TreeItemDTO<>( + AnalysisResultSearchParam.getTypeId(), + new FileTypeMimeSearchParams(searchParam.getMimeType(), this.dataSourceId), + mimeSuffix, + mimeSuffix, + originalTreeItem.getDisplayCount()); + } return null; } @Override - public int compare(FileTypeMimeSearchParams o1, FileTypeMimeSearchParams o2) { - return STRING_COMPARATOR.compare(o1.getMimeType(), o2.getMimeType()); + public int compare(TreeItemDTO o1, TreeItemDTO o2) { + return STRING_COMPARATOR.compare(o1.getSearchParams().getMimeType(), o2.getSearchParams().getMimeType()); } /** @@ -278,14 +349,46 @@ public class ViewsTypeFactory { } @Override - protected TreeResultsDTO.TreeItemDTO getOrCreateRelevantChild(TreeEvent daoEvt) { - //GVDTODO + protected void handleDAOAggregateEvent(DAOAggregateEvent aggEvt) { + for (DAOEvent evt : aggEvt.getEvents()) { + if (evt instanceof TreeEvent) { + TreeResultsDTO.TreeItemDTO treeItem = super.getTypedTreeItem((TreeEvent) evt, FileTypeExtensionsSearchParams.class); + // if search params has null filter, trigger full refresh + if (treeItem != null && treeItem.getSearchParams().getFilter() == null) { + super.update(); + return; + } + } + } + + super.handleDAOAggregateEvent(aggEvt); + } + + @Override + protected TreeResultsDTO.TreeItemDTO getOrCreateRelevantChild(TreeEvent treeEvt) { + TreeResultsDTO.TreeItemDTO originalTreeItem = super.getTypedTreeItem(treeEvt, FileTypeExtensionsSearchParams.class); + + if (originalTreeItem != null + // if filter is null, this should trigger a full refresh which should be handled in handleDAOAggregateEvent + && originalTreeItem.getSearchParams().getFilter() != null + && this.childFilters.contains(originalTreeItem.getSearchParams().getFilter()) + && (this.dataSourceId == null || Objects.equals(this.dataSourceId, originalTreeItem.getSearchParams().getDataSourceId()))) { + + // generate new type so that if it is a subtree event (i.e. keyword hits), the right tree item is created. + FileTypeExtensionsSearchParams searchParam = originalTreeItem.getSearchParams(); + return new TreeResultsDTO.TreeItemDTO<>( + AnalysisResultSearchParam.getTypeId(), + new FileTypeExtensionsSearchParams(searchParam.getFilter(), this.dataSourceId), + searchParam.getFilter(), + searchParam.getFilter().getDisplayName(), + originalTreeItem.getDisplayCount()); + } return null; } @Override - public int compare(FileTypeExtensionsSearchParams o1, FileTypeExtensionsSearchParams o2) { - return STRING_COMPARATOR.compare(o1.getFilter().getDisplayName(), o2.getFilter().getDisplayName()); + public int compare(TreeItemDTO o1, TreeItemDTO o2) { + return STRING_COMPARATOR.compare(o1.getSearchParams().getFilter().getDisplayName(), o2.getSearchParams().getFilter().getDisplayName()); } /**