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