diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/annotations/AnnotationUtils.java b/Core/src/org/sleuthkit/autopsy/contentviewers/annotations/AnnotationUtils.java index 97cf7d7e6d..a3320d2e02 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/annotations/AnnotationUtils.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/annotations/AnnotationUtils.java @@ -135,7 +135,7 @@ public class AnnotationUtils { * @return The pair of artifact (or null if not present) and content (either * artifact parent content, the node content, or null). */ - private static Pair getDisplayContent(Node node) { + static DisplayTskItems getDisplayContent(Node node) { BlackboardArtifactItem artItem = node.getLookup().lookup(BlackboardArtifactItem.class); BlackboardArtifact artifact = artItem == null ? null : artItem.getTskContent(); @@ -143,16 +143,18 @@ public class AnnotationUtils { ? artItem.getSourceContent() : node.getLookup().lookup(AbstractFile.class); - return Pair.of(artifact, content); + return new DisplayTskItems(artifact, content); } /** * Returns whether or not the node is supported by the annotation viewer. + * * @param node The node to display. + * * @return True if the node is supported. */ public static boolean isSupported(Node node) { - return getDisplayContent(node).getRight() != null; + return getDisplayContent(node).getContent() != null; } /** @@ -168,9 +170,9 @@ public class AnnotationUtils { Document html = Jsoup.parse(EMPTY_HTML); Element body = html.getElementsByTag("body").first(); - Pair displayPair = getDisplayContent(node); - BlackboardArtifact artifact = displayPair.getLeft(); - Content srcContent = displayPair.getRight(); + DisplayTskItems displayItems = getDisplayContent(node); + BlackboardArtifact artifact = displayItems.getArtifact(); + Content srcContent = displayItems.getContent(); boolean somethingWasRendered = false; if (artifact != null) { @@ -677,4 +679,39 @@ public class AnnotationUtils { } } + /** + * The TSK items that are being displayed as deciphered from the netbeans + * node. + */ + static class DisplayTskItems { + + private final BlackboardArtifact artifact; + private final Content content; + + /** + * Main constructor. + * + * @param artifact The artifact being displayed or null. + * @param content The parent content or source file being displayed or + * null. + */ + DisplayTskItems(BlackboardArtifact artifact, Content content) { + this.artifact = artifact; + this.content = content; + } + + /** + * @return The selected artifact or null if no selected artifact. + */ + BlackboardArtifact getArtifact() { + return artifact; + } + + /** + * @return The parent content or source file being displayed or null. + */ + Content getContent() { + return content; + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/annotations/AnnotationsContentViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/annotations/AnnotationsContentViewer.java index faed21e1b1..c01548026b 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/annotations/AnnotationsContentViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/annotations/AnnotationsContentViewer.java @@ -18,10 +18,16 @@ */ package org.sleuthkit.autopsy.contentviewers.annotations; +import com.google.common.collect.ImmutableSet; import java.awt.Component; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.EnumSet; +import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.logging.Level; import javax.swing.SwingWorker; +import org.apache.commons.lang3.tuple.Pair; import static org.openide.util.NbBundle.Messages; import org.openide.nodes.Node; @@ -29,8 +35,19 @@ import org.openide.util.lookup.ServiceProvider; import org.sleuthkit.autopsy.corecomponentinterfaces.DataContentViewer; import org.sleuthkit.autopsy.coreutils.Logger; import org.jsoup.nodes.Document; +import org.openide.util.WeakListeners; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.events.BlackBoardArtifactTagAddedEvent; +import org.sleuthkit.autopsy.casemodule.events.BlackBoardArtifactTagDeletedEvent; +import org.sleuthkit.autopsy.casemodule.events.CommentChangedEvent; +import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; +import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; +import org.sleuthkit.autopsy.contentviewers.annotations.AnnotationUtils.DisplayTskItems; import org.sleuthkit.autopsy.contentviewers.layout.ContentViewerHtmlStyles; import org.sleuthkit.autopsy.contentviewers.utils.ViewerPriority; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.autopsy.ingest.ModuleDataEvent; +import org.sleuthkit.datamodel.BlackboardArtifact; /** * Annotations view of file contents. @@ -47,7 +64,75 @@ public class AnnotationsContentViewer extends javax.swing.JPanel implements Data private static final long serialVersionUID = 1L; private static final Logger logger = Logger.getLogger(AnnotationsContentViewer.class.getName()); - private AnnotationWorker worker; + private static final Set CASE_EVENTS_OF_INTEREST = EnumSet.of( + Case.Events.BLACKBOARD_ARTIFACT_TAG_ADDED, + Case.Events.BLACKBOARD_ARTIFACT_TAG_DELETED, + Case.Events.CONTENT_TAG_ADDED, + Case.Events.CONTENT_TAG_DELETED, + Case.Events.CR_COMMENT_CHANGED); + + private static final Set INGEST_MODULE_EVENTS_OF_INTEREST = EnumSet.of(IngestManager.IngestModuleEvent.DATA_ADDED); + + private static final Set ARTIFACT_TYPES_OF_INTEREST = ImmutableSet.of( + BlackboardArtifact.Type.TSK_HASHSET_HIT, + BlackboardArtifact.Type.TSK_INTERESTING_FILE_HIT + ); + + private final PropertyChangeListener ingestEventListener = (evt) -> { + Long curArtifactId = AnnotationsContentViewer.this.curArtifactId; + Long curContentId = AnnotationsContentViewer.this.curContentId; + + if (curArtifactId == null && curContentId == null) { + return; + } + + // if it is a module data event + if (IngestManager.IngestModuleEvent.DATA_ADDED.toString().equals(evt.getPropertyName()) + && evt.getOldValue() instanceof ModuleDataEvent) { + + ModuleDataEvent moduleDataEvent = (ModuleDataEvent) evt.getOldValue(); + + // if an artifact is relevant, refresh + if (ARTIFACT_TYPES_OF_INTEREST.contains(moduleDataEvent.getBlackboardArtifactType())) { + for (BlackboardArtifact artifact : moduleDataEvent.getArtifacts()) { + if ((curArtifactId != null && artifact.getArtifactID() == curArtifactId) + || (curContentId != null && artifact.getObjectID() == curContentId)) { + refresh(); + return; + } + } + } + } + }; + + private final PropertyChangeListener weakIngestEventListener = WeakListeners.propertyChange(ingestEventListener, null); + + private final PropertyChangeListener caseEventListener = (evt) -> { + Long curArtifactId = AnnotationsContentViewer.this.curArtifactId; + Long curContentId = AnnotationsContentViewer.this.curContentId; + + if (curArtifactId == null && curContentId == null) { + return; + } + + Pair artifactContentId = getIdsFromEvent(evt); + Long artifactId = artifactContentId.getLeft(); + Long contentId = artifactContentId.getRight(); + + // if there is a match of content id or artifact id and the event, refresh + if ((curArtifactId != null && curArtifactId.equals(artifactId)) || (curContentId != null && curContentId.equals(contentId))) { + refresh(); + } + }; + + private final PropertyChangeListener weakCaseEventListener = WeakListeners.propertyChange(caseEventListener, null); + + private final Object updateLock = new Object(); + + private AnnotationWorker worker = null; + private Node node; + private Long curArtifactId; + private Long curContentId; /** * Creates an instance of AnnotationsContentViewer. @@ -55,23 +140,138 @@ public class AnnotationsContentViewer extends javax.swing.JPanel implements Data public AnnotationsContentViewer() { initComponents(); ContentViewerHtmlStyles.setupHtmlJTextPane(textPanel); + registerListeners(); + } + + /** + * Registers case event and ingest event listeners. + */ + private void registerListeners() { + Case.addEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, weakCaseEventListener); + IngestManager.getInstance().addIngestModuleEventListener(INGEST_MODULE_EVENTS_OF_INTEREST, weakIngestEventListener); + } + + @Override + protected void finalize() throws Throwable { + unregisterListeners(); + } + + /** + * Unregisters case event and ingest event listeners. + */ + private void unregisterListeners() { + Case.removeEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, weakCaseEventListener); + IngestManager.getInstance().addIngestModuleEventListener(INGEST_MODULE_EVENTS_OF_INTEREST, weakIngestEventListener); } @Override public void setNode(Node node) { - resetComponent(); + this.node = node; + DisplayTskItems displayItems = AnnotationUtils.getDisplayContent(node); + this.curArtifactId = displayItems.getArtifact() == null ? null : displayItems.getArtifact().getArtifactID(); + this.curContentId = displayItems.getContent() == null ? null : displayItems.getContent().getId(); + updateData(this.node, true); + } - if (worker != null) { - worker.cancel(true); - worker = null; + /** + * Returns a pair of the artifact id (or null) and the content id (or null) + * for the case event. + * + * @param evt The case event. + * + * @return A pair of the artifact id (or null) and the content id (or null) + * for the case event. + */ + private static Pair getIdsFromEvent(PropertyChangeEvent evt) { + Case.Events eventType = null; + try { + eventType = Case.Events.valueOf(evt.getPropertyName()); + } catch (IllegalArgumentException ex) { + logger.log(Level.SEVERE, "Unknown event type: " + evt.getPropertyName(), ex); + return Pair.of(null, null); } + Long artifactId = null; + Long contentId = null; + + switch (eventType) { + case BLACKBOARD_ARTIFACT_TAG_ADDED: + if (evt instanceof BlackBoardArtifactTagAddedEvent) { + BlackboardArtifact art = ((BlackBoardArtifactTagAddedEvent) evt).getAddedTag().getArtifact(); + artifactId = art.getArtifactID(); + contentId = art.getObjectID(); + } + break; + case BLACKBOARD_ARTIFACT_TAG_DELETED: + if (evt instanceof BlackBoardArtifactTagDeletedEvent) { + artifactId = ((BlackBoardArtifactTagDeletedEvent) evt).getDeletedTagInfo().getArtifactID(); + contentId = ((BlackBoardArtifactTagDeletedEvent) evt).getDeletedTagInfo().getContentID(); + } + break; + case CONTENT_TAG_ADDED: + if (evt instanceof ContentTagAddedEvent) { + contentId = ((ContentTagAddedEvent) evt).getAddedTag().getContent().getId(); + } + break; + case CONTENT_TAG_DELETED: + if (evt instanceof ContentTagDeletedEvent) { + contentId = ((ContentTagDeletedEvent) evt).getDeletedTagInfo().getContentID(); + } + break; + case CR_COMMENT_CHANGED: + if (evt instanceof CommentChangedEvent) { + long commentObjId = ((CommentChangedEvent) evt).getContentID(); + artifactId = commentObjId; + contentId = commentObjId; + } + break; + default: + break; + }; + + return Pair.of(artifactId, contentId); + } + + /** + * Refreshes the data displayed. + */ + private void refresh() { + if (this.isVisible()) { + updateData(this.node, false); + } + } + + /** + * Updates data displayed in the viewer. + * + * @param node The node to use for data. + * @param forceReset If true, forces a reset cancelling the previous worker + * if one exists and clearing data in the component. If + * false, only submits a worker if no previous worker is + * running. + */ + private void updateData(Node node, boolean forceReset) { if (node == null) { return; } - worker = new AnnotationWorker(node); - worker.execute(); + if (forceReset) { + resetComponent(); + } + + synchronized (updateLock) { + if (worker != null) { + if (forceReset) { + worker.cancel(true); + worker = null; + } else { + return; + } + } + + worker = new AnnotationWorker(node, forceReset); + worker.execute(); + } } /** @@ -142,6 +342,7 @@ public class AnnotationsContentViewer extends javax.swing.JPanel implements Data @Override public void resetComponent() { textPanel.setText(""); + } /** @@ -151,9 +352,18 @@ public class AnnotationsContentViewer extends javax.swing.JPanel implements Data private class AnnotationWorker extends SwingWorker { private final Node node; + private final boolean resetCaretPosition; - AnnotationWorker(Node node) { + /** + * Main constructor. + * + * @param node The node for which data will be fetched. + * @param resetCaretPosition Whether or not to reset the caret position + * when finished. + */ + AnnotationWorker(Node node, boolean resetCaretPosition) { this.node = node; + this.resetCaretPosition = resetCaretPosition; } @Override @@ -173,17 +383,25 @@ public class AnnotationsContentViewer extends javax.swing.JPanel implements Data @Override public void done() { - if (isCancelled()) { - return; + if (!isCancelled()) { + try { + String text = get(); + ContentViewerHtmlStyles.setStyles(textPanel); + textPanel.setText(text); + + if (resetCaretPosition) { + textPanel.setCaretPosition(0); + } + + } catch (InterruptedException | ExecutionException ex) { + logger.log(Level.SEVERE, "Failed to get annotation information for node", ex); + } } - try { - String text = get(); - ContentViewerHtmlStyles.setStyles(textPanel); - textPanel.setText(text); - textPanel.setCaretPosition(0); - } catch (InterruptedException | ExecutionException ex) { - logger.log(Level.SEVERE, "Failed to get annotation information for node", ex); + synchronized (updateLock) { + if (worker == this) { + worker = null; + } } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactItem.java b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactItem.java index 9748c3b747..9a4d9e8c22 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactItem.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactItem.java @@ -23,13 +23,13 @@ import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.Content; /** - * An super class for an Autopsy Data Model item class with an underlying - * BlackboardArtifact Sleuth Kit Data Model object, i.e., a DataArtifact or an - * AnalysisResult. + * An abstract super class for an Autopsy Data Model item class with an + * underlying BlackboardArtifact Sleuth Kit Data Model object, i.e., a + * DataArtifact or an AnalysisResult. * - * @param The concrete BlackboardArtifact class type. + * @param The concrete BlackboardArtifact sub class type. */ -public class BlackboardArtifactItem extends TskContentItem { +public abstract class BlackboardArtifactItem extends TskContentItem { private final Content sourceContent; diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java index 5ce277036f..354978f7e5 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java @@ -37,7 +37,6 @@ import java.util.Map; import java.util.MissingResourceException; import java.util.Set; import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.stream.Collectors; @@ -159,7 +158,6 @@ public class BlackboardArtifactNode extends AbstractContentNode artifactItem; if (artifact instanceof AnalysisResult) { artifactItem = new AnalysisResultItem((AnalysisResult) artifact, content); - } else if (artifact instanceof DataArtifact) { - artifactItem = new DataArtifactItem((DataArtifact) artifact, content); } else { - artifactItem = new BlackboardArtifactItem<>(artifact, content); + artifactItem = new DataArtifactItem((DataArtifact) artifact, content); } /* @@ -862,8 +852,8 @@ public class BlackboardArtifactNode extends AbstractContentNode( + Bundle.BlackboardArtifactNode_createSheet_srcFile_name(), + Bundle.BlackboardArtifactNode_createSheet_srcFile_displayName(), + NO_DESCR, + getDisplayName())); GetSCOTask scoTask = null; if (artifact instanceof AnalysisResult @@ -1417,7 +1419,7 @@ public class BlackboardArtifactNode extends AbstractContentNode