From 7af7e34c1c12f3176994d723aa9da735ea2f3c15 Mon Sep 17 00:00:00 2001 From: Greg DiCristofaro <gregd@basistech.com> Date: Thu, 21 Oct 2021 13:21:55 -0400 Subject: [PATCH] integrated event listener into data result panel --- Core/ivy.xml | 3 - Core/nbproject/project.properties | 2 - Core/nbproject/project.xml | 8 - .../corecomponents/DataResultPanel.java | 29 ++- .../mainui/datamodel/AnalysisResultDAO.java | 6 +- .../mainui/datamodel/DataArtifactDAO.java | 2 +- .../mainui/datamodel/EventUpdatableCache.java | 109 ++++++++- .../datamodel/EventUpdatableCacheImpl.java | 194 --------------- .../autopsy/mainui/datamodel/ViewsDAO.java | 8 +- .../mainui/nodes/SearchResultSupport.java | 229 ++++++++++++++---- 10 files changed, 326 insertions(+), 264 deletions(-) delete mode 100644 Core/src/org/sleuthkit/autopsy/mainui/datamodel/EventUpdatableCacheImpl.java diff --git a/Core/ivy.xml b/Core/ivy.xml index 810bb5d411..5173860c32 100644 --- a/Core/ivy.xml +++ b/Core/ivy.xml @@ -57,9 +57,6 @@ <!-- for handling diffs --> <dependency org="io.github.java-diff-utils" name="java-diff-utils" rev="4.8"/> - <!-- for MainUI event updates --> - <dependency org="io.projectreactor" name="reactor-core" rev="3.4.11"/> - <dependency org="org.reactivestreams" name="reactive-streams" rev="1.0.3"/> <!-- https://mvnrepository.com/artifact/javax.ws.rs/javax.ws.rs-api --> <dependency conf="core->default" org="javax.ws.rs" name="javax.ws.rs-api" rev="2.0"/> diff --git a/Core/nbproject/project.properties b/Core/nbproject/project.properties index eb3431bb0c..5377cc3d82 100644 --- a/Core/nbproject/project.properties +++ b/Core/nbproject/project.properties @@ -25,8 +25,6 @@ file.reference.commons-lang3-3.5.jar=release\\modules\\ext\\commons-lang3-3.5.ja file.reference.commons-logging-1.2.jar=release\\modules\\ext\\commons-logging-1.2.jar file.reference.commons-pool2-2.4.2.jar=release\\modules\\ext\\commons-pool2-2.4.2.jar file.reference.java-diff-utils-4.8.jar=release\\modules\\ext\\java-diff-utils-4.8.jar -file.reference.reactor-core-3.4.11.jar=release\\modules\\ext\\reactor-core-3.4.11.jar -file.reference.reactive-streams-1.0.3.jar=release\\modules\\ext\\reactive-streams-1.0.3.jar file.reference.commons-validator-1.6.jar=release\\modules\\ext\\commons-validator-1.6.jar file.reference.curator-client-2.8.0.jar=release\\modules\\ext\\curator-client-2.8.0.jar file.reference.curator-framework-2.8.0.jar=release\\modules\\ext\\curator-framework-2.8.0.jar diff --git a/Core/nbproject/project.xml b/Core/nbproject/project.xml index 00c1eab9b3..67d26d0cb7 100644 --- a/Core/nbproject/project.xml +++ b/Core/nbproject/project.xml @@ -657,14 +657,6 @@ <runtime-relative-path>ext/java-diff-utils-4.8.jar</runtime-relative-path> <binary-origin>release\modules\ext\java-diff-utils-4.8.jar</binary-origin> </class-path-extension> - <class-path-extension> - <runtime-relative-path>ext/reactor-core-3.4.11.jar</runtime-relative-path> - <binary-origin>release\modules\ext\reactor-core-3.4.11.jar</binary-origin> - </class-path-extension> - <class-path-extension> - <runtime-relative-path>ext/reactive-streams-1.0.3.jar</runtime-relative-path> - <binary-origin>release\modules\ext\reactive-streams-1.0.3.jar</binary-origin> - </class-path-extension> <class-path-extension> <runtime-relative-path>ext/SparseBitSet-1.1.jar</runtime-relative-path> <binary-origin>release\modules\ext\SparseBitSet-1.1.jar</binary-origin> diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultPanel.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultPanel.java index a64fb0486d..b969cd455f 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultPanel.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultPanel.java @@ -29,6 +29,7 @@ import java.util.EnumSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.logging.Level; @@ -45,6 +46,7 @@ import org.openide.util.Lookup; import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; +import org.openide.util.WeakListeners; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.core.UserPreferences; @@ -57,6 +59,7 @@ import org.sleuthkit.autopsy.datamodel.BaseChildFactory.PageCountChangeEvent; import org.sleuthkit.autopsy.datamodel.BaseChildFactory.PageSizeChangeEvent; import org.sleuthkit.autopsy.datamodel.NodeSelectionInfo; +import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.mainui.datamodel.DataArtifactSearchParam; import org.sleuthkit.autopsy.mainui.datamodel.FileTypeExtensionsSearchParams; import org.sleuthkit.autopsy.mainui.datamodel.FileTypeMimeSearchParams; @@ -133,13 +136,29 @@ public class DataResultPanel extends javax.swing.JPanel implements DataResult, C } } }; - + private final PropertyChangeListener caseCloseListener = evt -> { if (evt.getNewValue() == null) { nodeNameToPageCountListenerMap.clear(); } }; + private final PropertyChangeListener weakCaseCloseListener = WeakListeners.propertyChange(caseCloseListener, null); + + private static final Set<IngestManager.IngestModuleEvent> INGEST_MODULE_EVENTS = EnumSet.of(IngestManager.IngestModuleEvent.CONTENT_CHANGED, IngestManager.IngestModuleEvent.DATA_ADDED); + + private final PropertyChangeListener ingestModuleListener = evt -> { + if (this.searchResultSupport.isRefreshRequired(evt)) { + try { + displaySearchResults(this.searchResultSupport.getRefreshedData(), false); + } catch (ExecutionException | IllegalArgumentException ex) { + logger.log(Level.WARNING, "There was an error refreshing data: ", ex); + } + } + }; + + private final PropertyChangeListener weakIngestModuleListener = WeakListeners.propertyChange(ingestModuleListener, null); + /** * Creates and opens a Swing JPanel with a JTabbedPane child component that * contains instances of the result viewers (DataResultViewer) provided by @@ -293,7 +312,8 @@ private static void createInstanceCommon(String title, String description, Node private void initListeners() { UserPreferences.addChangeListener(this.pageSizeListener); - Case.addEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), this.caseCloseListener); + Case.addEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), this.weakCaseCloseListener); + IngestManager.getInstance().addIngestModuleEventListener(INGEST_MODULE_EVENTS, this.weakIngestModuleListener); } /** @@ -661,6 +681,11 @@ void close() { this.removeAll(); this.setVisible(false); } + + UserPreferences.removeChangeListener(this.pageSizeListener); + Case.removeEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), this.weakCaseCloseListener); + IngestManager.getInstance().removeIngestModuleEventListener(INGEST_MODULE_EVENTS, this.weakIngestModuleListener); + } @Override diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/AnalysisResultDAO.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/AnalysisResultDAO.java index 118c72cb50..b5d5ead3e9 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/AnalysisResultDAO.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/AnalysisResultDAO.java @@ -107,7 +107,7 @@ synchronized static AnalysisResultDAO getInstance() { private final AnalysisResultSetCache<HashHitSearchParam> hashHitCache = new AnalysisResultSetCache<>(BlackboardArtifact.Type.TSK_HASHSET_HIT); private final AnalysisResultSetCache<KeywordHitSearchParam> keywordHitCache = new AnalysisResultSetCache<>(BlackboardArtifact.Type.TSK_KEYWORD_HIT); - private final List<EventUpdatableCacheImpl<?, ?, ModuleDataEvent>> caches = ImmutableList.of(analysisResultCache, hashHitCache, keywordHitCache); + private final List<EventUpdatableCache<?, ?, ModuleDataEvent>> caches = ImmutableList.of(analysisResultCache, hashHitCache, keywordHitCache); @Override void addAnalysisResultColumnKeys(List<ColumnKey> columnKeys) { @@ -220,7 +220,7 @@ public boolean isKeywordHitsInvalidating(KeywordHitSearchParam artifactKey, Modu return keywordHitCache.isInvalidatingEvent(artifactKey, evt); } - private class AnalysisResultCache extends EventUpdatableCacheImpl<AnalysisResultSearchParam, AnalysisResultTableSearchResultsDTO, ModuleDataEvent> { + private class AnalysisResultCache extends EventUpdatableCache<AnalysisResultSearchParam, AnalysisResultTableSearchResultsDTO, ModuleDataEvent> { @Override protected AnalysisResultTableSearchResultsDTO fetch(AnalysisResultSearchParam cacheKey) throws Exception { @@ -274,7 +274,7 @@ protected boolean isCacheRelevantEvent(ModuleDataEvent eventData) { } private class AnalysisResultSetCache<K extends AnalysisResultSetSearchParam> extends - EventUpdatableCacheImpl<K, AnalysisResultTableSearchResultsDTO, ModuleDataEvent> { + EventUpdatableCache<K, AnalysisResultTableSearchResultsDTO, ModuleDataEvent> { private final BlackboardArtifact.Type artifactType; diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/DataArtifactDAO.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/DataArtifactDAO.java index f0bc5dd468..e6fbd3d9d9 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/DataArtifactDAO.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/DataArtifactDAO.java @@ -78,7 +78,7 @@ public DataArtifactTableSearchResultsDTO getDataArtifactsForTable(DataArtifactSe } - private class DataArtifactCache extends EventUpdatableCacheImpl<DataArtifactSearchParam, DataArtifactTableSearchResultsDTO, ModuleDataEvent> { + private class DataArtifactCache extends EventUpdatableCache<DataArtifactSearchParam, DataArtifactTableSearchResultsDTO, ModuleDataEvent> { @Override protected DataArtifactTableSearchResultsDTO fetch(DataArtifactSearchParam cacheKey) throws Exception { diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/EventUpdatableCache.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/EventUpdatableCache.java index f412b414f4..1ac6592575 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/EventUpdatableCache.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/EventUpdatableCache.java @@ -18,17 +18,44 @@ */ package org.sleuthkit.autopsy.mainui.datamodel; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; /** - * The public API for a cache of key value pairs where an event has the + * A partial implementation of a cache of key value pairs where an event has the * potential to invalidate particular cache entries. * * @param <K> The key type. * @param <V> The value type. * @param <E> The event type. */ -public interface EventUpdatableCache<K, V, E> { +abstract class EventUpdatableCache<K, V, E> { + + private static final int DEFAULT_CACHE_SIZE = 15; // rule of thumb: 5 entries times number of cached SearchParams sub-types + private static final long DEFAULT_CACHE_DURATION = 2; + private static final TimeUnit CACHE_DURATION_UNITS = TimeUnit.MINUTES; + + private final Cache<K, V> cache; + + /** + * Constructor with default underlying cache. + */ + EventUpdatableCache() { + this(CacheBuilder.newBuilder() + .maximumSize(DEFAULT_CACHE_SIZE) + .expireAfterAccess(DEFAULT_CACHE_DURATION, CACHE_DURATION_UNITS) + .build()); + } + + /** + * Constructor. + * @param cache Non-default cache to use as underlying data source. + */ + EventUpdatableCache(Cache<K, V> cache) { + this.cache = cache; + } /** * Returns the value in the cache for the given key. If the key is not @@ -41,7 +68,9 @@ public interface EventUpdatableCache<K, V, E> { * @throws IllegalArgumentException * @throws ExecutionException */ - V getValue(K key) throws IllegalArgumentException, ExecutionException; + V getValue(K key) throws IllegalArgumentException, ExecutionException { + return cache.get(key, () -> fetch(key)); + } /** * Returns the value in the cache for the given key. If the key is not @@ -57,7 +86,76 @@ public interface EventUpdatableCache<K, V, E> { * @throws IllegalArgumentException * @throws ExecutionException */ - V getValue(K key, boolean hardRefresh) throws IllegalArgumentException, ExecutionException; + V getValue(K key, boolean hardRefresh) throws IllegalArgumentException, ExecutionException { + validateCacheKey(key); + if (hardRefresh) { + cache.invalidate(key); + } + + return cache.get(key, () -> fetch(key)); + } + + /** + * Invalidates all cached entries. + */ + void invalidateAll() { + cache.invalidateAll(); + } + + /** + * Invalidates all cached entries where this event may have affected the + * data. + * + * @param eventData The event data. + */ + void invalidate(E eventData) { + if (!isCacheRelevantEvent(eventData)) { + return; + } + + cache.asMap().replaceAll((k,v) -> isInvalidatingEvent(k, eventData) ? null : v); + } + + /** + * Validates that the cache key meets invariants for fetching data or throws + * an illegal argument exception. This method disallows null keys at this + * time but can be overridden for specialized behavior. + * + * @param key The key. + * + * @throws IllegalArgumentException + */ + protected void validateCacheKey(K key) throws IllegalArgumentException { + if (key == null) { + throw new IllegalArgumentException("Expected non-null key"); + } + } + + /** + * This method short cuts iterating over all keys to see if an event + * invalidates a key if there is no way that the event will affect a key in + * a cache. This method returns true by default but can be overridden for + * specialized behavior. + * + * @param eventData The event data. + * + * @return True if this event could potentially impact the cache. + */ + protected boolean isCacheRelevantEvent(E eventData) { + // to be overridden + return true; + } + + /** + * Fetches data from the database for the given search parameters key. + * + * @param key The key. + * + * @return The retrieved value. + * + * @throws Exception + */ + protected abstract V fetch(K key) throws Exception; /** * Returns true if the event data would invalidate the data for the @@ -68,6 +166,5 @@ public interface EventUpdatableCache<K, V, E> { * * @return True if the event data invalidates the cached data for the key. */ - boolean isInvalidatingEvent(K key, E eventData); - + abstract boolean isInvalidatingEvent(K key, E eventData); } diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/EventUpdatableCacheImpl.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/EventUpdatableCacheImpl.java deleted file mode 100644 index df87f371d8..0000000000 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/EventUpdatableCacheImpl.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2021 Basis Technology Corp. - * Contact: carrier <at> sleuthkit <dot> org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.sleuthkit.autopsy.mainui.datamodel; - -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -import java.text.MessageFormat; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.sleuthkit.autopsy.coreutils.Logger; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Sinks; -import reactor.core.publisher.Sinks.EmitResult; -import reactor.util.concurrent.Queues; - -/** - * A partial implementation of a cache of key value pairs where an event has the - * potential to invalidate particular cache entries. - * - * @param <K> The key type. - * @param <V> The value type. - * @param <E> The event type. - */ -abstract class EventUpdatableCacheImpl<K, V, E> implements EventUpdatableCache<K, V, E> { - - private static final Logger logger = Logger.getLogger(EventUpdatableCacheImpl.class.getName()); - - private static final int DEFAULT_CACHE_SIZE = 15; // rule of thumb: 5 entries times number of cached SearchParams sub-types - private static final long DEFAULT_CACHE_DURATION = 2; - private static final TimeUnit CACHE_DURATION_UNITS = TimeUnit.MINUTES; - - private final Cache<K, V> cache; - - // taken from https://stackoverflow.com/questions/66671636/why-is-sinks-many-multicast-onbackpressurebuffer-completing-after-one-of-t - private final Sinks.Many<Set<K>> invalidatedKeyMulticast = Sinks.many().multicast().onBackpressureBuffer(Queues.SMALL_BUFFER_SIZE, false); - - public EventUpdatableCacheImpl() { - this(CacheBuilder.newBuilder() - .maximumSize(DEFAULT_CACHE_SIZE) - .expireAfterAccess(DEFAULT_CACHE_DURATION, CACHE_DURATION_UNITS) - .build()); - } - - protected EventUpdatableCacheImpl(Cache<K, V> cache) { - this.cache = cache; - } - - @Override - public V getValue(K key) throws IllegalArgumentException, ExecutionException { - return cache.get(key, () -> fetch(key)); - } - - @Override - public V getValue(K key, boolean hardRefresh) throws IllegalArgumentException, ExecutionException { - validateCacheKey(key); - if (hardRefresh) { - // GVDTODO handle as transaction - V value; - try { - value = fetch(key); - } catch (Exception ex) { - throw new ExecutionException("Unable to fetch key: " + key, ex); - } - cache.put(key, value); - return value; - } else { - return cache.get(key, () -> fetch(key)); - } - } - - private V getValueLoggedError(K key) { - try { - return getValue(key); - } catch (IllegalArgumentException | ExecutionException ex) { - logger.log(Level.WARNING, "An error occurred while fetching results for key: " + key, ex); - return null; - } - } - - public Flux<V> getInitialAndUpdates(K key) throws IllegalArgumentException { - validateCacheKey(key); - - // GVDTODO handle in one transaction - Flux<V> initial = Flux.fromStream(Stream.of(getValueLoggedError(key))); - - Flux<V> updates = this.invalidatedKeyMulticast.asFlux() - .filter(invalidatedKeys -> invalidatedKeys.contains(key)) - .map((matchingInvalidatedKey) -> getValueLoggedError(key)); - - return Flux.concat(initial, updates) - .filter((data) -> data != null); - } - - /** - * Invalidates all cached entries. - */ - void invalidateAll() { - Set<K> keys = new HashSet<>(cache.asMap().keySet()); - invalidateAndBroadcast(keys); - } - - /** - * Invalidates all cached entries where this event may have affected the - * data. - * - * @param eventData The event data. - */ - void invalidate(E eventData) { - if (!isCacheRelevantEvent(eventData)) { - return; - } - - Set<K> keys = cache.asMap().keySet().stream() - .filter((key) -> isInvalidatingEvent(key, eventData)) - .collect(Collectors.toSet()); - invalidateAndBroadcast(keys); - } - - private void invalidateAndBroadcast(Set<K> keys) { - if (keys.isEmpty()) { - return; - } - - cache.invalidateAll(keys); - EmitResult emitResult = invalidatedKeyMulticast.tryEmitNext(keys); - if (emitResult.isFailure()) { - logger.log(Level.WARNING, MessageFormat.format("There was an error broadcasting invalidated keys: {0}", emitResult.name())); - } - - } - - /** - * Validates that the cache key meets invariants for fetching data or throws - * an illegal argument exception. This method disallows null keys at this - * time but can be overridden for specialized behavior. - * - * @param key The key. - * - * @throws IllegalArgumentException - */ - protected void validateCacheKey(K key) throws IllegalArgumentException { - if (key == null) { - throw new IllegalArgumentException("Expected non-null key"); - } - } - - /** - * This method short cuts iterating over all keys to see if an event - * invalidates a key if there is no way that the event will affect a key in - * a cache. This method returns true by default but can be overridden for - * specialized behavior. - * - * @param eventData The event data. - * - * @return True if this event could potentially impact the cache. - */ - protected boolean isCacheRelevantEvent(E eventData) { - // to be overridden - return true; - } - - /** - * Fetches data from the database for the given search parameters key. - * - * @param key The key. - * - * @return The retrieved value. - * - * @throws Exception - */ - protected abstract V fetch(K key) throws Exception; - -} diff --git a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/ViewsDAO.java b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/ViewsDAO.java index c46efe4e2f..2e98eb2c04 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/datamodel/ViewsDAO.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/datamodel/ViewsDAO.java @@ -152,7 +152,7 @@ private static ExtensionMediaType getExtensionMediaType(String ext) { private final FilesByExtensionCache extensionCache = new FilesByExtensionCache(); private final FilesByMimeCache mimeCache = new FilesByMimeCache(); private final FilesBySizeCache sizeCache = new FilesBySizeCache(); - private final List<EventUpdatableCacheImpl<?, ?, Content>> caches = ImmutableList.of(extensionCache, mimeCache, sizeCache); + private final List<EventUpdatableCache<?, ?, Content>> caches = ImmutableList.of(extensionCache, mimeCache, sizeCache); public SearchResultsDTO getFilesByExtension(FileTypeExtensionsSearchParams key) throws ExecutionException, IllegalArgumentException { return this.extensionCache.getValue(key); @@ -281,7 +281,7 @@ protected void onContentChange(Content content) { caches.forEach((cache) -> cache.invalidate(content)); } - private class FilesByExtensionCache extends EventUpdatableCacheImpl<FileTypeExtensionsSearchParams, SearchResultsDTO, Content> { + private class FilesByExtensionCache extends EventUpdatableCache<FileTypeExtensionsSearchParams, SearchResultsDTO, Content> { private String getFileExtensionWhereStatement(FileExtSearchFilter filter, Long dataSourceId) { String whereClause = "(dir_type = " + TskData.TSK_FS_NAME_TYPE_ENUM.REG.getValue() + ")" @@ -322,7 +322,7 @@ protected void validateCacheKey(FileTypeExtensionsSearchParams key) throws Illeg } } - private class FilesByMimeCache extends EventUpdatableCacheImpl<FileTypeMimeSearchParams, SearchResultsDTO, Content> { + private class FilesByMimeCache extends EventUpdatableCache<FileTypeMimeSearchParams, SearchResultsDTO, Content> { private String getFileMimeWhereStatement(String mimeType, Long dataSourceId) { String whereClause = "(dir_type = " + TskData.TSK_FS_NAME_TYPE_ENUM.REG.getValue() + ")" @@ -369,7 +369,7 @@ protected void validateCacheKey(FileTypeMimeSearchParams key) throws IllegalArgu } } - private class FilesBySizeCache extends EventUpdatableCacheImpl<FileTypeSizeSearchParams, SearchResultsDTO, Content> { + private class FilesBySizeCache extends EventUpdatableCache<FileTypeSizeSearchParams, SearchResultsDTO, Content> { private String getFileSizesWhereStatement(FileTypeSizeSearchParams.FileSizeFilter filter, Long dataSourceId) { String lowerBound = "size >= " + filter.getLowerBound(); diff --git a/Core/src/org/sleuthkit/autopsy/mainui/nodes/SearchResultSupport.java b/Core/src/org/sleuthkit/autopsy/mainui/nodes/SearchResultSupport.java index d1eb057499..c340595cd2 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/nodes/SearchResultSupport.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/nodes/SearchResultSupport.java @@ -21,11 +21,16 @@ import java.beans.PropertyChangeEvent; import java.text.MessageFormat; import java.util.concurrent.ExecutionException; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.autopsy.ingest.ModuleContentEvent; +import org.sleuthkit.autopsy.ingest.ModuleDataEvent; import org.sleuthkit.autopsy.mainui.datamodel.DataArtifactSearchParam; import org.sleuthkit.autopsy.mainui.datamodel.FileTypeExtensionsSearchParams; import org.sleuthkit.autopsy.mainui.datamodel.FileTypeMimeSearchParams; +import org.sleuthkit.autopsy.mainui.datamodel.SearchParams; import org.sleuthkit.autopsy.mainui.datamodel.MainDAO; import org.sleuthkit.autopsy.mainui.datamodel.SearchResultsDTO; +import org.sleuthkit.datamodel.Content; /** * Provides functionality to handle paging and fetching of search result data. @@ -36,7 +41,7 @@ public class SearchResultSupport { private int pageIdx = 0; private SearchResultsDTO currentSearchResults = null; - private PageFetcher pageFetcher = null; + private DataFetcher pageFetcher = null; private final MainDAO dao = MainDAO.getInstance(); /** @@ -134,7 +139,7 @@ public SearchResultsDTO getCurrentSearchResults() { public synchronized SearchResultsDTO updatePageSize(int pageSize) throws IllegalArgumentException, ExecutionException { this.pageIdx = 0; setPageSize(pageSize); - return fetchResults(); + return fetchResults(false); } /** @@ -150,7 +155,7 @@ public synchronized SearchResultsDTO updatePageSize(int pageSize) throws Illegal */ public synchronized SearchResultsDTO updatePageIdx(int pageIdx) throws IllegalArgumentException, ExecutionException { setPageIdx(pageIdx); - return fetchResults(); + return fetchResults(false); } /** @@ -196,6 +201,43 @@ public synchronized void clearSearchParameters() { this.currentSearchResults = null; } + /** + * Determines if a refresh is required for the currently selected item. + * + * @param evt The ingest module event. + * + * @return True if an update is required. + */ + public synchronized boolean isRefreshRequired(PropertyChangeEvent evt) { + return isRefreshRequired(this.pageFetcher, evt); + } + + private synchronized <S extends SearchParams, E> boolean isRefreshRequired(DataFetcher<S, E> dataFetcher, PropertyChangeEvent evt) { + if (dataFetcher == null) { + return false; + } + + E evtData = dataFetcher.extractEvtData(evt); + + if (evtData == null) { + return false; + } + + S curKey = dataFetcher.getParams(pageSize, pageIdx); + return dataFetcher.isRefreshRequired(curKey, evtData); + } + + /** + * Forces a refresh of data based on current search parameters. + * + * @return The refreshed data. + * + * @throws ExecutionException + */ + public synchronized SearchResultsDTO getRefreshedData() throws ExecutionException { + return fetchResults(true); + } + /** * Fetches results using current page fetcher or returns null if no current * page fetcher. Also stores current results in local variable. @@ -204,9 +246,9 @@ public synchronized void clearSearchParameters() { * * @throws ExecutionException */ - private synchronized SearchResultsDTO fetchResults() throws ExecutionException { + private synchronized SearchResultsDTO fetchResults(boolean hardRefresh) throws ExecutionException { SearchResultsDTO newResults = (this.pageFetcher != null) - ? this.pageFetcher.fetch(this.pageSize, this.pageIdx) + ? this.pageFetcher.fetch(this.pageFetcher.getParams(pageSize, pageIdx), hardRefresh) : null; this.currentSearchResults = newResults; @@ -218,55 +260,90 @@ private synchronized void resetPaging() { } /** - * Sets the search parameters to the file type extension search parameters. + * Sets the search parameters to the data artifact search parameters. * Subsequent calls that don't change search parameters (i.e. page size * changes, page index changes) will use these search parameters to return * results. * - * @param fileExtParameters The file type extension search parameters. + * @param dataArtifactParameters The data artifact search parameters. * * @return The results of querying with current paging parameters. * * @throws ExecutionException */ - public synchronized SearchResultsDTO setFileExtensions(final FileTypeExtensionsSearchParams fileExtParameters) throws ExecutionException { + public synchronized SearchResultsDTO setDataArtifact(final DataArtifactSearchParam dataArtifactParameters) throws ExecutionException { resetPaging(); - this.pageFetcher = (pageSize, pageIdx) -> { - FileTypeExtensionsSearchParams searchParams = new FileTypeExtensionsSearchParams( - fileExtParameters.getFilter(), - fileExtParameters.getDataSourceId(), - pageIdx * pageSize, - (long) pageSize); - return dao.getViewsDAO().getFilesByExtension(searchParams); + + this.pageFetcher = new DataFetcher<DataArtifactSearchParam, ModuleDataEvent>() { + @Override + public DataArtifactSearchParam getParams(int pageSize, int pageIdx) { + return new DataArtifactSearchParam( + dataArtifactParameters.getArtifactType(), + dataArtifactParameters.getDataSourceId(), + pageIdx * pageSize, + (long) pageSize); + } + + @Override + public SearchResultsDTO fetch(DataArtifactSearchParam searchParams, boolean hardRefresh) throws ExecutionException { + return dao.getDataArtifactsDAO().getDataArtifactsForTable(searchParams, hardRefresh); + } + + @Override + public ModuleDataEvent extractEvtData(PropertyChangeEvent evt) { + return getModuleDataFromEvt(evt); + } + + @Override + public boolean isRefreshRequired(DataArtifactSearchParam searchParams, ModuleDataEvent evtData) { + return dao.getDataArtifactsDAO().isDataArtifactInvalidating(searchParams, evtData); + } }; - return fetchResults(); + return fetchResults(false); } /** - * Sets the search parameters to the data artifact search parameters. + * Sets the search parameters to the file type extension search parameters. * Subsequent calls that don't change search parameters (i.e. page size * changes, page index changes) will use these search parameters to return * results. * - * @param dataArtifactParameters The data artifact search parameters. + * @param fileExtParameters The file type extension search parameters. * * @return The results of querying with current paging parameters. * * @throws ExecutionException */ - public synchronized SearchResultsDTO setDataArtifact(final DataArtifactSearchParam dataArtifactParameters) throws ExecutionException { + public synchronized SearchResultsDTO setFileExtensions(final FileTypeExtensionsSearchParams fileExtParameters) throws ExecutionException { resetPaging(); - this.pageFetcher = (pageSize, pageIdx) -> { - DataArtifactSearchParam searchParams = new DataArtifactSearchParam( - dataArtifactParameters.getArtifactType(), - dataArtifactParameters.getDataSourceId(), - pageIdx * pageSize, - (long) pageSize); - return dao.getDataArtifactsDAO().getDataArtifactsForTable(searchParams); + this.pageFetcher = new DataFetcher<FileTypeExtensionsSearchParams, Content>() { + @Override + public FileTypeExtensionsSearchParams getParams(int pageSize, int pageIdx) { + return new FileTypeExtensionsSearchParams( + fileExtParameters.getFilter(), + fileExtParameters.getDataSourceId(), + pageIdx * pageSize, + (long) pageSize); + } + + @Override + public SearchResultsDTO fetch(FileTypeExtensionsSearchParams searchParams, boolean hardRefresh) throws ExecutionException { + return dao.getViewsDAO().getFilesByExtension(searchParams, hardRefresh); + } + + @Override + public Content extractEvtData(PropertyChangeEvent evt) { + return getContentFromEvt(evt); + } + + @Override + public boolean isRefreshRequired(FileTypeExtensionsSearchParams searchParams, Content evtData) { + return dao.getViewsDAO().isFilesByExtInvalidating(searchParams, evtData); + } }; - return fetchResults(); + return fetchResults(false); } /** @@ -284,35 +361,105 @@ public synchronized SearchResultsDTO setDataArtifact(final DataArtifactSearchPar */ public synchronized SearchResultsDTO setFileMimes(FileTypeMimeSearchParams fileMimeKey) throws ExecutionException, IllegalArgumentException { resetPaging(); - this.pageFetcher = (pageSize, pageIdx) -> { - FileTypeMimeSearchParams searchParams = new FileTypeMimeSearchParams( - fileMimeKey.getMimeType(), - fileMimeKey.getDataSourceId(), - pageIdx * pageSize, - (long) pageSize); - return dao.getViewsDAO().getFilesByMime(searchParams); + this.pageFetcher = new DataFetcher<FileTypeMimeSearchParams, Content>() { + @Override + public FileTypeMimeSearchParams getParams(int pageSize, int pageIdx) { + return new FileTypeMimeSearchParams( + fileMimeKey.getMimeType(), + fileMimeKey.getDataSourceId(), + pageIdx * pageSize, + (long) pageSize); + } + + @Override + public SearchResultsDTO fetch(FileTypeMimeSearchParams searchParams, boolean hardRefresh) throws ExecutionException { + return dao.getViewsDAO().getFilesByMime(searchParams, hardRefresh); + } + + @Override + public Content extractEvtData(PropertyChangeEvent evt) { + return getContentFromEvt(evt); + } + + @Override + public boolean isRefreshRequired(FileTypeMimeSearchParams searchParams, Content evtData) { + return dao.getViewsDAO().isFilesByMimeInvalidating(searchParams, evtData); + } }; - return fetchResults(); + return fetchResults(false); + } + + private static Content getContentFromEvt(PropertyChangeEvent evt) { + String eventName = evt.getPropertyName(); + if (IngestManager.IngestModuleEvent.CONTENT_CHANGED.toString().equals(eventName) + && (evt.getOldValue() instanceof ModuleContentEvent) + && ((ModuleContentEvent) evt.getOldValue()).getSource() instanceof Content) { + + return (Content) ((ModuleContentEvent) evt.getOldValue()).getSource(); + + } else { + return null; + } + } + + private static ModuleDataEvent getModuleDataFromEvt(PropertyChangeEvent evt) { + String eventName = evt.getPropertyName(); + if (IngestManager.IngestModuleEvent.DATA_ADDED.toString().equals(eventName) + && (evt.getOldValue() instanceof ModuleDataEvent)) { + + return (ModuleDataEvent) evt.getOldValue(); + } else { + return null; + } } /** * Means of fetching data based on paging settings. */ - private interface PageFetcher { + private interface DataFetcher<S extends SearchParams, D> { /** - * Fetches search results data based on paging settings. + * Returns the search parameters based on the page size and page index. * - * @param pageSize The page size. + * @param pageSize The number of items per page. * @param pageIdx The page index. * + * @return The search parameters. + */ + S getParams(int pageSize, int pageIdx); + + /** + * Fetches search results data based on paging settings. + * + * + * @param searchParams The search parameters. + * @param hardRefresh Whether or not to perform a hard refresh. + * * @return The retrieved data. * * @throws ExecutionException */ - SearchResultsDTO fetch(int pageSize, int pageIdx) throws ExecutionException; + SearchResultsDTO fetch(S searchParams, boolean hardRefresh) throws ExecutionException; + + /** + * Extracts pertinent data from the property change event. + * + * @param evt The event. + * + * @return The extracted data. If null, refresh will not be required. + */ + D extractEvtData(PropertyChangeEvent evt); + + /** + * Returns true if the ingest module event will require a refresh in the + * data. + * + * @param searchParams The search parameters. + * @param evtData The event data. + * + * @return True if the + */ + boolean isRefreshRequired(S searchParams, D evtData); } - - } -- GitLab