diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/AnnotationsContentViewer.form b/Core/src/org/sleuthkit/autopsy/contentviewers/AnnotationsContentViewer.form
index 4fe61b2fb5..98fb50c89a 100755
--- a/Core/src/org/sleuthkit/autopsy/contentviewers/AnnotationsContentViewer.form
+++ b/Core/src/org/sleuthkit/autopsy/contentviewers/AnnotationsContentViewer.form
@@ -21,21 +21,25 @@
-
+
-
+
-
+
+
+
+
+
-
+
diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/AnnotationsContentViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/AnnotationsContentViewer.java
index 5a4725a8d7..5d109438c3 100755
--- a/Core/src/org/sleuthkit/autopsy/contentviewers/AnnotationsContentViewer.java
+++ b/Core/src/org/sleuthkit/autopsy/contentviewers/AnnotationsContentViewer.java
@@ -19,43 +19,23 @@
package org.sleuthkit.autopsy.contentviewers;
import java.awt.Component;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.function.Function;
+import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
-import java.util.stream.Collectors;
import javax.swing.JLabel;
+import javax.swing.SwingWorker;
import javax.swing.text.EditorKit;
import javax.swing.text.html.HTMLEditorKit;
-import org.apache.commons.lang.StringUtils;
-import org.apache.commons.lang3.tuple.Pair;
import static org.openide.util.NbBundle.Messages;
-import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE;
-import org.sleuthkit.autopsy.coreutils.Logger;
import org.openide.nodes.Node;
import org.openide.util.lookup.ServiceProvider;
-import org.sleuthkit.autopsy.casemodule.Case;
-import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
-import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance;
-import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeNormalizationException;
-import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeUtil;
-import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException;
import org.sleuthkit.autopsy.corecomponentinterfaces.DataContentViewer;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.BlackboardArtifact;
-import org.sleuthkit.datamodel.BlackboardArtifactTag;
-import org.sleuthkit.datamodel.Content;
-import org.sleuthkit.datamodel.ContentTag;
-import org.sleuthkit.datamodel.SleuthkitCase;
-import org.sleuthkit.datamodel.Tag;
import org.sleuthkit.datamodel.TskCoreException;
-import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository;
-import org.sleuthkit.datamodel.BlackboardAttribute;
-import org.jsoup.Jsoup;
+import org.sleuthkit.autopsy.contentviewers.application.Annotations;
+import org.sleuthkit.autopsy.coreutils.Logger;
import org.jsoup.nodes.Document;
-import org.jsoup.nodes.Element;
/**
* Annotations view of file contents.
@@ -65,90 +45,10 @@ import org.jsoup.nodes.Element;
@Messages({
"AnnotationsContentViewer.title=Annotations",
"AnnotationsContentViewer.toolTip=Displays tags and comments associated with the selected content.",
- "AnnotationsContentViewer.centralRepositoryEntry.title=Central Repository Comments",
- "AnnotationsContentViewer.centralRepositoryEntryDataLabel.case=Case:",
- "AnnotationsContentViewer.centralRepositoryEntryDataLabel.type=Type:",
- "AnnotationsContentViewer.centralRepositoryEntryDataLabel.comment=Comment:",
- "AnnotationsContentViewer.centralRepositoryEntryDataLabel.path=Path:",
- "AnnotationsContentViewer.tagEntry.title=Tags",
- "AnnotationsContentViewer.tagEntryDataLabel.tag=Tag:",
- "AnnotationsContentViewer.tagEntryDataLabel.tagUser=Examiner:",
- "AnnotationsContentViewer.tagEntryDataLabel.comment=Comment:",
- "AnnotationsContentViewer.fileHitEntry.artifactCommentTitle=Artifact Comment",
- "AnnotationsContentViewer.fileHitEntry.hashSetHitTitle=Hash Set Hit Comments",
- "AnnotationsContentViewer.fileHitEntry.interestingFileHitTitle=Interesting File Hit Comments",
- "AnnotationsContentViewer.fileHitEntry.setName=Set Name:",
- "AnnotationsContentViewer.fileHitEntry.comment=Comment:",
- "AnnotationsContentViewer.sourceFile.title=Source File",
"AnnotationsContentViewer.onEmpty=No annotations were found for this particular item."
})
public class AnnotationsContentViewer extends javax.swing.JPanel implements DataContentViewer {
- /**
- * Describes a key value pair for an item of type T where the key is the
- * field name to display and the value is retrieved from item of type T
- * using a provided Function.
- *
- * @param The item type.
- */
- private static class ItemEntry {
-
- private final String itemName;
- private final Function valueRetriever;
-
- ItemEntry(String itemName, Function valueRetriever) {
- this.itemName = itemName;
- this.valueRetriever = valueRetriever;
- }
-
- String getItemName() {
- return itemName;
- }
-
- Function getValueRetriever() {
- return valueRetriever;
- }
-
- String retrieveValue(T object) {
- return valueRetriever.apply(object);
- }
- }
-
- /**
- * Describes a section that will be appended to the annotations view panel.
- *
- * @param The item type for items to display.
- */
- private static class SectionConfig {
-
- private final String title;
- private final List> attributes;
-
- SectionConfig(String title, List> attributes) {
- this.title = title;
- this.attributes = attributes;
- }
-
- /**
- * @return The title for the section.
- */
- String getTitle() {
- return title;
- }
-
- /**
- * @return Describes key-value pairs on the object to display to the
- * user.
- */
- List> getAttributes() {
- return attributes;
- }
- }
-
- private static final Logger logger = Logger.getLogger(AnnotationsContentViewer.class.getName());
-
- private static final String EMPTY_HTML = "";
-
private static final int DEFAULT_FONT_SIZE = new JLabel().getFont().getSize();
// how big the subheader should be
@@ -161,77 +61,34 @@ public class AnnotationsContentViewer extends javax.swing.JPanel implements Data
private static final int DEFAULT_SUBSECTION_LEFT_PAD = DEFAULT_FONT_SIZE;
// spacing occurring after an item
- private static final int DEFAULT_TABLE_SPACING = DEFAULT_FONT_SIZE;
private static final int DEFAULT_SECTION_SPACING = DEFAULT_FONT_SIZE * 2;
private static final int DEFAULT_SUBSECTION_SPACING = DEFAULT_FONT_SIZE / 2;
private static final int CELL_SPACING = DEFAULT_FONT_SIZE / 2;
- // html stylesheet classnames for components
- private static final String MESSAGE_CLASSNAME = "message";
- private static final String SUBSECTION_CLASSNAME = "subsection";
- private static final String SUBHEADER_CLASSNAME = "subheader";
- private static final String SECTION_CLASSNAME = "section";
- private static final String HEADER_CLASSNAME = "header";
- private static final String VERTICAL_TABLE_CLASSNAME = "vertical-table";
-
// additional styling for components
private static final String STYLE_SHEET_RULE
- = String.format(" .%s { font-size: %dpx;font-style:italic; margin: 0px; padding: 0px; } ", MESSAGE_CLASSNAME, DEFAULT_FONT_SIZE)
+ = String.format(" .%s { font-size: %dpx;font-style:italic; margin: 0px; padding: 0px; } ", Annotations.MESSAGE_CLASSNAME, DEFAULT_FONT_SIZE)
+ String.format(" .%s {font-size:%dpx;font-weight:bold; margin: 0px; margin-top: %dpx; padding: 0px; } ",
- SUBHEADER_CLASSNAME, SUBHEADER_FONT_SIZE, DEFAULT_SUBSECTION_SPACING)
- + String.format(" .%s { font-size:%dpx;font-weight:bold; margin: 0px; padding: 0px; } ", HEADER_CLASSNAME, HEADER_FONT_SIZE)
+ Annotations.SUBHEADER_CLASSNAME, SUBHEADER_FONT_SIZE, DEFAULT_SUBSECTION_SPACING)
+ + String.format(" .%s { font-size:%dpx;font-weight:bold; margin: 0px; padding: 0px; } ", Annotations.HEADER_CLASSNAME, HEADER_FONT_SIZE)
+ String.format(" td { vertical-align: top; font-size:%dpx; text-align: left; margin: 0px; padding: 0px %dpx 0px 0px;} ", DEFAULT_FONT_SIZE, CELL_SPACING)
+ String.format(" th { vertical-align: top; text-align: left; margin: 0px; padding: 0px %dpx 0px 0px} ", DEFAULT_FONT_SIZE, CELL_SPACING)
- + String.format(" .%s { margin: %dpx 0px; padding-left: %dpx; } ", SUBSECTION_CLASSNAME, DEFAULT_SUBSECTION_SPACING, DEFAULT_SUBSECTION_LEFT_PAD)
- + String.format(" .%s { margin-bottom: %dpx; } ", SECTION_CLASSNAME, DEFAULT_SECTION_SPACING);
-
- // describing table values for a tag
- private static final List> TAG_ENTRIES = Arrays.asList(
- new ItemEntry<>(Bundle.AnnotationsContentViewer_tagEntryDataLabel_tag(),
- (tag) -> (tag.getName() != null) ? tag.getName().getDisplayName() : null),
- new ItemEntry<>(Bundle.AnnotationsContentViewer_tagEntryDataLabel_tagUser(), (tag) -> tag.getUserName()),
- new ItemEntry<>(Bundle.AnnotationsContentViewer_tagEntryDataLabel_comment(), (tag) -> tag.getComment())
- );
-
- private static final SectionConfig TAG_CONFIG
- = new SectionConfig<>(Bundle.AnnotationsContentViewer_tagEntry_title(), TAG_ENTRIES);
-
- // file set attributes and table configurations
- private static final List> FILESET_HIT_ENTRIES = Arrays.asList(
- new ItemEntry<>(Bundle.AnnotationsContentViewer_fileHitEntry_setName(),
- (bba) -> tryGetAttribute(bba, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME)),
- new ItemEntry<>(Bundle.AnnotationsContentViewer_fileHitEntry_comment(),
- (bba) -> tryGetAttribute(bba, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COMMENT))
- );
-
- private static final SectionConfig INTERESTING_FILE_CONFIG
- = new SectionConfig<>(Bundle.AnnotationsContentViewer_fileHitEntry_interestingFileHitTitle(), FILESET_HIT_ENTRIES);
-
- private static final SectionConfig HASHSET_CONFIG
- = new SectionConfig<>(Bundle.AnnotationsContentViewer_fileHitEntry_hashSetHitTitle(), FILESET_HIT_ENTRIES);
-
- private static final SectionConfig ARTIFACT_COMMENT_CONFIG
- = new SectionConfig<>(Bundle.AnnotationsContentViewer_fileHitEntry_artifactCommentTitle(), FILESET_HIT_ENTRIES);
-
- // central repository attributes and table configuration
- private static final List> CR_COMMENTS_ENTRIES = Arrays.asList(
- new ItemEntry<>(Bundle.AnnotationsContentViewer_centralRepositoryEntryDataLabel_case(),
- cai -> (cai.getCorrelationCase() != null) ? cai.getCorrelationCase().getDisplayName() : null),
- new ItemEntry<>(Bundle.AnnotationsContentViewer_centralRepositoryEntryDataLabel_comment(), cai -> cai.getComment()),
- new ItemEntry<>(Bundle.AnnotationsContentViewer_centralRepositoryEntryDataLabel_path(), cai -> cai.getFilePath())
- );
-
- private static final SectionConfig CR_COMMENTS_CONFIG
- = new SectionConfig<>(Bundle.AnnotationsContentViewer_centralRepositoryEntry_title(), CR_COMMENTS_ENTRIES);
-
+ + String.format(" .%s { margin: %dpx 0px; padding-left: %dpx; } ", Annotations.SUBSECTION_CLASSNAME, DEFAULT_SUBSECTION_SPACING, DEFAULT_SUBSECTION_LEFT_PAD)
+ + String.format(" .%s { margin-bottom: %dpx; } ", Annotations.SECTION_CLASSNAME, DEFAULT_SECTION_SPACING);
+
+ private static final long serialVersionUID = 1L;
+ private static final Logger logger = Logger.getLogger(AnnotationsContentViewer.class.getName());
+
+ private AnnotationWorker worker;
+
/**
* Creates an instance of AnnotationsContentViewer.
*/
public AnnotationsContentViewer() {
initComponents();
- Utilities.configureTextPaneAsHtml(jTextPane1);
+ Utilities.configureTextPaneAsHtml(textPanel);
// get html editor kit and apply additional style rules
- EditorKit editorKit = jTextPane1.getEditorKit();
+ EditorKit editorKit = textPanel.getEditorKit();
if (editorKit instanceof HTMLEditorKit) {
HTMLEditorKit htmlKit = (HTMLEditorKit) editorKit;
htmlKit.getStyleSheet().addRule(STYLE_SHEET_RULE);
@@ -240,472 +97,21 @@ public class AnnotationsContentViewer extends javax.swing.JPanel implements Data
@Override
public void setNode(Node node) {
- if ((node == null) || (!isSupported(node))) {
- resetComponent();
+ resetComponent();
+
+ if(worker != null) {
+ worker.cancel(true);
+ worker = null;
+ }
+
+ if(node == null) {
return;
}
- Document html = Jsoup.parse(EMPTY_HTML);
- Element body = html.getElementsByTag("body").first();
-
- BlackboardArtifact artifact = node.getLookup().lookup(BlackboardArtifact.class);
- Content sourceFile = null;
-
- try {
- if (artifact != null) {
- /*
- * Get the source content based on the artifact to ensure we
- * display the correct data instead of whatever was in the node.
- */
- sourceFile = artifact.getSleuthkitCase().getAbstractFileById(artifact.getObjectID());
- } else {
- /*
- * No artifact is present, so get the content based on what's
- * present in the node. In this case, the selected item IS the
- * source file.
- */
- sourceFile = node.getLookup().lookup(AbstractFile.class);
- }
- } catch (TskCoreException ex) {
- logger.log(Level.SEVERE, String.format(
- "Exception while trying to retrieve a Content instance from the BlackboardArtifact '%s' (id=%d).",
- artifact.getDisplayName(), artifact.getArtifactID()), ex);
- }
-
- boolean somethingWasRendered = false;
- if (artifact != null) {
- somethingWasRendered = renderArtifact(body, artifact, sourceFile);
- } else {
- somethingWasRendered = renderContent(body, sourceFile, false);
- }
-
- if (!somethingWasRendered) {
- appendMessage(body, Bundle.AnnotationsContentViewer_onEmpty());
- }
-
- jTextPane1.setText(html.html());
- jTextPane1.setCaretPosition(0);
+ worker = new AnnotationWorker(node);
+ worker.execute();
}
- /**
- * Renders annotations for an artifact.
- *
- * @param parent The html element to render content int.
- * @param bba The blackboard artifact to render.
- * @param sourceContent The content from which the blackboard artifact
- * comes.
- *
- * @return If any content was actually rendered.
- */
- private static boolean renderArtifact(Element parent, BlackboardArtifact bba, Content sourceContent) {
- boolean contentRendered = appendEntries(parent, TAG_CONFIG, getTags(bba), false);
-
- if (CentralRepository.isEnabled()) {
- List centralRepoComments = getCentralRepositoryData(bba);
- boolean crRendered = appendEntries(parent, CR_COMMENTS_CONFIG, centralRepoComments, false);
- contentRendered = contentRendered || crRendered;
- }
-
- // if artifact is a hashset hit or interesting file and has a non-blank comment
- if ((ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID() == bba.getArtifactTypeID()
- || ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getTypeID() == bba.getArtifactTypeID())
- && (hasTskComment(bba))) {
-
- boolean filesetRendered = appendEntries(parent, ARTIFACT_COMMENT_CONFIG, Arrays.asList(bba), false);
- contentRendered = contentRendered || filesetRendered;
- }
-
- Element sourceFileSection = appendSection(parent, Bundle.AnnotationsContentViewer_sourceFile_title());
- boolean sourceFileRendered = renderContent(sourceFileSection, sourceContent, true);
-
- if (!sourceFileRendered) {
- sourceFileSection.remove();
- }
-
- return contentRendered || sourceFileRendered;
- }
-
- /**
- * Renders annotations for a content item.
- *
- * @param parent The parent within which to render.
- * @param sourceContent The content for which annotations will be gathered.
- * @param isSubheader True if this section should be rendered as a
- * subheader as opposed to a top-level header.
- *
- * @return If any content was actually rendered.
- */
- private static boolean renderContent(Element parent, Content sourceContent, boolean isSubheader) {
- boolean contentRendered = appendEntries(parent, TAG_CONFIG, getTags(sourceContent), isSubheader);
-
- if (sourceContent instanceof AbstractFile) {
- AbstractFile sourceFile = (AbstractFile) sourceContent;
-
- if (CentralRepository.isEnabled()) {
- List centralRepoComments = getCentralRepositoryData(sourceFile);
- boolean crRendered = appendEntries(parent, CR_COMMENTS_CONFIG, centralRepoComments, isSubheader);
- contentRendered = contentRendered || crRendered;
- }
-
- boolean hashsetRendered = appendEntries(parent, HASHSET_CONFIG,
- getFileSetHits(sourceFile, ARTIFACT_TYPE.TSK_HASHSET_HIT),
- isSubheader);
-
- boolean interestingFileRendered = appendEntries(parent, INTERESTING_FILE_CONFIG,
- getFileSetHits(sourceFile, ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT),
- isSubheader);
-
- contentRendered = contentRendered || hashsetRendered || interestingFileRendered;
- }
- return contentRendered;
- }
-
- /**
- * Retrieves tags associated with a content item.
- *
- * @param sourceContent The content for which to gather content.
- *
- * @return The Tags associated with this item.
- */
- private static List getTags(Content sourceContent) {
- try {
- SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase();
- return tskCase.getContentTagsByContent(sourceContent);
- } catch (NoCurrentCaseException ex) {
- logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS
- } catch (TskCoreException ex) {
- logger.log(Level.SEVERE, "Exception while getting tags from the case database.", ex); //NON-NLS
- }
- return new ArrayList<>();
- }
-
- /**
- * Retrieves tags for blackboard artifact tags.
- *
- * @param bba The blackboard artifact for which to retrieve tags.
- *
- * @return The found tags.
- */
- private static List getTags(BlackboardArtifact bba) {
- try {
- SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase();
- return tskCase.getBlackboardArtifactTagsByArtifact(bba);
- } catch (NoCurrentCaseException ex) {
- logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS
- } catch (TskCoreException ex) {
- logger.log(Level.SEVERE, "Exception while getting tags from the case database.", ex); //NON-NLS
- }
- return new ArrayList<>();
- }
-
- /**
- * Retrieves the blackboard artifacts for a source file matching a certain
- * type that have a non-blank TSK_COMMENT.
- *
- * @param sourceFile The source file for which to fetch artifacts.
- * @param type The type of blackboard artifact to fetch.
- *
- * @return The artifacts found matching this type.
- */
- private static List getFileSetHits(AbstractFile sourceFile, ARTIFACT_TYPE type) {
- try {
- SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase();
- return tskCase.getBlackboardArtifacts(type, sourceFile.getId()).stream()
- .filter((bba) -> hasTskComment(bba))
- .collect(Collectors.toList());
- } catch (NoCurrentCaseException ex) {
- logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS
- } catch (TskCoreException ex) {
- logger.log(Level.SEVERE, "Exception while getting file set hits from the case database.", ex); //NON-NLS
- }
- return new ArrayList<>();
- }
-
- /**
- * Returns true if the artifact contains a non-blank TSK_COMMENT attribute.
- *
- * @param artifact The artifact to check.
- *
- * @return True if it has a non-blank TSK_COMMENT.
- */
- private static boolean hasTskComment(BlackboardArtifact artifact) {
- return StringUtils.isNotBlank(tryGetAttribute(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COMMENT));
- }
-
- /**
- * Attempts to retrieve the attribute of a particular type from a blackboard
- * artifact.
- *
- * @param artifact The artifact from which to retrieve the information.
- * @param attributeType The attribute type to retrieve from the artifact.
- *
- * @return The string value of the attribute or null if not found.
- */
- private static String tryGetAttribute(BlackboardArtifact artifact, BlackboardAttribute.ATTRIBUTE_TYPE attributeType) {
- if (artifact == null) {
- return null;
- }
-
- BlackboardAttribute attr = null;
- try {
- attr = artifact.getAttribute(new BlackboardAttribute.Type(attributeType));
- } catch (TskCoreException ex) {
- logger.log(Level.WARNING, String.format("Unable to fetch attribute of type %s for artifact %s", attributeType, artifact), ex);
- }
-
- if (attr == null) {
- return null;
- }
-
- return attr.getValueString();
- }
-
- /**
- * Gets the "Central Repository Comments" section with data for the
- * blackboard artifact.
- *
- * @param artifact The selected artifact.
- *
- * @return The Correlation Attribute Instances associated with the artifact
- * that have comments.
- */
- private static List getCentralRepositoryData(BlackboardArtifact artifact) {
- if (artifact == null) {
- return new ArrayList<>();
- }
-
- List> lookupKeys = CorrelationAttributeUtil.makeCorrAttrsForCorrelation(artifact)
- .stream()
- .map(cai -> Pair.of(cai.getCorrelationType(), cai.getCorrelationValue()))
- .collect(Collectors.toList());
-
- return getCorrelationAttributeComments(lookupKeys);
- }
-
- /**
- * Gets the "Central Repository Comments" section with data.
- *
- * @param sourceFile A selected file, or a source file of the selected
- * artifact.
- *
- * @return The Correlation Attribute Instances associated with the
- * sourcefile that have comments.
- */
- private static List getCentralRepositoryData(AbstractFile sourceFile) {
- if (sourceFile == null || StringUtils.isEmpty(sourceFile.getMd5Hash())) {
- return new ArrayList<>();
- }
-
- List artifactTypes = null;
- try {
- artifactTypes = CentralRepository.getInstance().getDefinedCorrelationTypes();
- } catch (CentralRepoException ex) {
- logger.log(Level.SEVERE, "Error connecting to the Central Repository database.", ex); // NON-NLS
- }
-
- if (artifactTypes == null || artifactTypes.isEmpty()) {
- return new ArrayList<>();
- }
-
- String md5 = sourceFile.getMd5Hash();
-
- // get key lookups for a file attribute types and the md5 hash
- List> lookupKeys = artifactTypes.stream()
- .filter((attributeType) -> attributeType.getId() == CorrelationAttributeInstance.FILES_TYPE_ID)
- .map((attributeType) -> Pair.of(attributeType, md5))
- .collect(Collectors.toList());
-
- return getCorrelationAttributeComments(lookupKeys);
- }
-
- /**
- * Given a type and a value for that type, does a lookup in the Central
- * Repository for matching values that have comments.
- *
- * @param lookupKeys The type and value to lookup.
- *
- * @return The found correlation attribute instances.
- */
- private static List getCorrelationAttributeComments(List> lookupKeys) {
- List instancesToRet = new ArrayList<>();
-
- try {
- // use lookup instances to find the actual correlation attributes for the items selected
- for (Pair typeVal : lookupKeys) {
- instancesToRet.addAll(CentralRepository.getInstance()
- .getArtifactInstancesByTypeValue(typeVal.getKey(), typeVal.getValue())
- .stream()
- // for each one found, if it has a comment, return
- .filter((cai) -> StringUtils.isNotBlank(cai.getComment()))
- .collect(Collectors.toList()));
- }
-
- } catch (CentralRepoException ex) {
- logger.log(Level.SEVERE, "Error connecting to the Central Repository database.", ex); // NON-NLS
- } catch (CorrelationAttributeNormalizationException ex) {
- logger.log(Level.SEVERE, "Error normalizing instance from Central Repository database.", ex); // NON-NLS
- }
-
- return instancesToRet;
- }
-
- /**
- * Append entries to the parent element in the annotations viewer. Entries
- * will be formatted as a table in the format specified in the
- * SectionConfig.
- *
- * @param parent The parent element for which the entries will be
- * attached.
- * @param config The display configuration for this entry type (i.e.
- * table type, name, if data is not present).
- * @param items The items to display.
- * @param isSubsection Whether or not this should be displayed as a
- * subsection. If not displayed as a top-level section.
- *
- * @return If there was actual content rendered for this set of entries.
- */
- private static boolean appendEntries(Element parent, SectionConfig config, List extends T> items,
- boolean isSubsection) {
- if (items == null || items.isEmpty()) {
- return false;
- }
-
- Element sectionDiv = (isSubsection) ? appendSubsection(parent, config.getTitle()) : appendSection(parent, config.getTitle());
- appendVerticalEntryTables(sectionDiv, items, config.getAttributes());
- return true;
- }
-
- /**
- * Appends a table where items are displayed in rows of key-value pairs.
- *
- * @param parent The parent to append the table.
- * @param items The items to process into a series of tables.
- * @param rowHeaders The keys and the means to process items in order to get
- * key-value pairs.
- *
- * @return The parent element provided as parameter.
- */
- private static Element appendVerticalEntryTables(Element parent, List extends T> items, List> rowHeaders) {
- boolean isFirst = true;
- for (T item : items) {
- if (item == null) {
- continue;
- }
-
- List> tableData = rowHeaders.stream()
- .map(row -> Arrays.asList(row.getItemName(), row.retrieveValue(item)))
- .collect(Collectors.toList());
-
- Element childTable = appendTable(parent, 2, tableData, null);
- childTable.attr("class", VERTICAL_TABLE_CLASSNAME);
-
- if (isFirst) {
- isFirst = false;
- } else {
- childTable.attr("style", String.format("margin-top: %dpx;", DEFAULT_TABLE_SPACING));
- }
- }
-
- return parent;
- }
-
- /**
- * Appends a generic table to the parent element.
- *
- * @param parent The parent element that will have a table appended
- * to it.
- * @param columnNumber The number of columns to append.
- * @param content The content in content.get(row).get(column) format.
- * @param columnHeaders The column headers or null if no column headers
- * should be created.
- *
- * @return The created table.
- */
- private static Element appendTable(Element parent, int columnNumber, List> content, List columnHeaders) {
- Element table = parent.appendElement("table");
- if (columnHeaders != null && !columnHeaders.isEmpty()) {
- Element header = table.appendElement("thead");
- appendRow(header, columnHeaders, columnNumber, true);
- }
- Element tableBody = table.appendElement("tbody");
-
- content.forEach((rowData) -> appendRow(tableBody, rowData, columnNumber, false));
- return table;
- }
-
- /**
- * Appends a row to the parent element (should be thead or tbody).
- *
- * @param rowParent The parent table element.
- * @param data The data to place in columns within the table.
- * @param columnNumber The number of columns to append.
- * @param isHeader Whether or not this should have header cells ('th')
- * instead of regular cells ('td').
- *
- * @return The row created.
- */
- private static Element appendRow(Element rowParent, List data, int columnNumber, boolean isHeader) {
- String cellType = isHeader ? "th" : "td";
- Element row = rowParent.appendElement("tr");
- for (int i = 0; i < columnNumber; i++) {
- Element cell = row.appendElement(cellType);
- if (data != null && i < data.size()) {
- cell.text(StringUtils.isEmpty(data.get(i)) ? "" : data.get(i));
- }
- }
- return row;
- }
-
- /**
- * Appends a new section with a section header to the parent element.
- *
- * @param parent The element to append this section to.
- * @param headerText The text for the section.
- *
- * @return The div for the new section.
- */
- private static Element appendSection(Element parent, String headerText) {
- Element sectionDiv = parent.appendElement("div");
- sectionDiv.attr("class", SECTION_CLASSNAME);
- Element header = sectionDiv.appendElement("h1");
- header.text(headerText);
- header.attr("class", HEADER_CLASSNAME);
- return sectionDiv;
- }
-
- /**
- * Appends a new subsection with a subsection header to the parent element.
- *
- * @param parent The element to append this subsection to.
- * @param headerText The text for the subsection.
- *
- * @return The div for the new subsection.
- */
- private static Element appendSubsection(Element parent, String headerText) {
- Element subsectionDiv = parent.appendElement("div");
- subsectionDiv.attr("class", SUBSECTION_CLASSNAME);
- Element header = subsectionDiv.appendElement("h2");
- header.text(headerText);
- header.attr("class", SUBHEADER_CLASSNAME);
- return subsectionDiv;
- }
-
- /**
- * Appends a message to the parent element. This is typically used in the
- * event that no data exists for a certain type.
- *
- * @param parent The parent element that will have this message appended to
- * it.
- * @param message The message to append.
- *
- * @return The paragraph element for the new message.
- */
- private static Element appendMessage(Element parent, String message) {
- Element messageEl = parent.appendElement("p");
- messageEl.text(message);
- messageEl.attr("class", MESSAGE_CLASSNAME);
- return messageEl;
- }
/**
* This method is called from within the constructor to initialize the form.
@@ -716,31 +122,30 @@ public class AnnotationsContentViewer extends javax.swing.JPanel implements Data
// //GEN-BEGIN:initComponents
private void initComponents() {
- jScrollPane5 = new javax.swing.JScrollPane();
- jTextPane1 = new javax.swing.JTextPane();
+ javax.swing.JScrollPane scrollPane = new javax.swing.JScrollPane();
+ textPanel = new javax.swing.JTextPane();
setPreferredSize(new java.awt.Dimension(100, 58));
- jTextPane1.setEditable(false);
- jTextPane1.setName(""); // NOI18N
- jTextPane1.setPreferredSize(new java.awt.Dimension(600, 52));
- jScrollPane5.setViewportView(jTextPane1);
+ textPanel.setEditable(false);
+ textPanel.setName(""); // NOI18N
+ textPanel.setPreferredSize(new java.awt.Dimension(600, 52));
+ scrollPane.setViewportView(textPanel);
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
this.setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
- .addComponent(jScrollPane5, javax.swing.GroupLayout.DEFAULT_SIZE, 907, Short.MAX_VALUE)
+ .addComponent(scrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 907, Short.MAX_VALUE)
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
- .addComponent(jScrollPane5, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 435, Short.MAX_VALUE)
+ .addComponent(scrollPane, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 435, Short.MAX_VALUE)
);
}// //GEN-END:initComponents
// Variables declaration - do not modify//GEN-BEGIN:variables
- private javax.swing.JScrollPane jScrollPane5;
- private javax.swing.JTextPane jTextPane1;
+ private javax.swing.JTextPane textPanel;
// End of variables declaration//GEN-END:variables
@Override
@@ -793,6 +198,49 @@ public class AnnotationsContentViewer extends javax.swing.JPanel implements Data
@Override
public void resetComponent() {
- jTextPane1.setText(EMPTY_HTML);
+ textPanel.setText("");
+ }
+
+ /**
+ * A SwingWorker that will fetch the annotation information for the given
+ * node.
+ */
+ private class AnnotationWorker extends SwingWorker {
+ private final Node node;
+
+ AnnotationWorker(Node node) {
+ this.node = node;
+ }
+
+ @Override
+ protected String doInBackground() throws Exception {
+ Document doc = Annotations.buildDocument(node);
+
+ if(isCancelled()) {
+ return null;
+ }
+
+ if(doc != null) {
+ return doc.html();
+ } else {
+ return Bundle.AnnotationsContentViewer_onEmpty();
+ }
+ }
+
+ @Override
+ public void done() {
+ if (isCancelled()) {
+ return;
+ }
+
+ try {
+ String text = get();
+ textPanel.setText(text);
+ textPanel.setCaretPosition(0);
+ } catch (InterruptedException | ExecutionException ex) {
+ logger.log(Level.SEVERE, "Failed to get annotation information for node", ex);
+ }
+ }
+
}
}
diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED
index 6df3399a0a..2a62423a64 100644
--- a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED
+++ b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED
@@ -15,22 +15,7 @@
# governing permissions and limitations under the License.
#
-AnnotationsContentViewer.centralRepositoryEntry.title=Central Repository Comments
-AnnotationsContentViewer.centralRepositoryEntryDataLabel.case=Case:
-AnnotationsContentViewer.centralRepositoryEntryDataLabel.comment=Comment:
-AnnotationsContentViewer.centralRepositoryEntryDataLabel.path=Path:
-AnnotationsContentViewer.centralRepositoryEntryDataLabel.type=Type:
-AnnotationsContentViewer.fileHitEntry.artifactCommentTitle=Artifact Comment
-AnnotationsContentViewer.fileHitEntry.comment=Comment:
-AnnotationsContentViewer.fileHitEntry.hashSetHitTitle=Hash Set Hit Comments
-AnnotationsContentViewer.fileHitEntry.interestingFileHitTitle=Interesting File Hit Comments
-AnnotationsContentViewer.fileHitEntry.setName=Set Name:
AnnotationsContentViewer.onEmpty=No annotations were found for this particular item.
-AnnotationsContentViewer.sourceFile.title=Source File
-AnnotationsContentViewer.tagEntry.title=Tags
-AnnotationsContentViewer.tagEntryDataLabel.comment=Comment:
-AnnotationsContentViewer.tagEntryDataLabel.tag=Tag:
-AnnotationsContentViewer.tagEntryDataLabel.tagUser=Examiner:
AnnotationsContentViewer.title=Annotations
AnnotationsContentViewer.toolTip=Displays tags and comments associated with the selected content.
ApplicationContentViewer.title=Application
diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/application/Annotations.java b/Core/src/org/sleuthkit/autopsy/contentviewers/application/Annotations.java
new file mode 100755
index 0000000000..feec2703dc
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/contentviewers/application/Annotations.java
@@ -0,0 +1,673 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2021 Basis Technology Corp.
+ * Contact: carrier sleuthkit 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.contentviewers.application;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Function;
+import java.util.logging.Level;
+import java.util.stream.Collectors;
+import javax.swing.JLabel;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.openide.nodes.Node;
+import org.openide.util.NbBundle;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
+import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException;
+import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository;
+import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance;
+import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeNormalizationException;
+import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeUtil;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.datamodel.AbstractFile;
+import org.sleuthkit.datamodel.BlackboardArtifact;
+import org.sleuthkit.datamodel.BlackboardArtifactTag;
+import org.sleuthkit.datamodel.BlackboardAttribute;
+import org.sleuthkit.datamodel.Content;
+import org.sleuthkit.datamodel.ContentTag;
+import org.sleuthkit.datamodel.SleuthkitCase;
+import org.sleuthkit.datamodel.Tag;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/**
+ * The business logic for the Annotations content panel.
+ */
+public class Annotations {
+
+ @NbBundle.Messages({
+ "Annotations.title=Annotations",
+ "Annotations.toolTip=Displays tags and comments associated with the selected content.",
+ "Annotations.centralRepositoryEntry.title=Central Repository Comments",
+ "Annotations.centralRepositoryEntryDataLabel.case=Case:",
+ "Annotations.centralRepositoryEntryDataLabel.type=Type:",
+ "Annotations.centralRepositoryEntryDataLabel.comment=Comment:",
+ "Annotations.centralRepositoryEntryDataLabel.path=Path:",
+ "Annotations.tagEntry.title=Tags",
+ "Annotations.tagEntryDataLabel.tag=Tag:",
+ "Annotations.tagEntryDataLabel.tagUser=Examiner:",
+ "Annotations.tagEntryDataLabel.comment=Comment:",
+ "Annotations.fileHitEntry.artifactCommentTitle=Artifact Comment",
+ "Annotations.fileHitEntry.hashSetHitTitle=Hash Set Hit Comments",
+ "Annotations.fileHitEntry.interestingFileHitTitle=Interesting File Hit Comments",
+ "Annotations.fileHitEntry.setName=Set Name:",
+ "Annotations.fileHitEntry.comment=Comment:",
+ "Annotations.sourceFile.title=Source File",
+ "Annotations.onEmpty=No annotations were found for this particular item."
+ })
+
+ private static final Logger logger = Logger.getLogger(Annotations.class.getName());
+
+ private static final String EMPTY_HTML = "";
+
+ private static final int DEFAULT_FONT_SIZE = new JLabel().getFont().getSize();
+ // spacing occurring after an item
+ private static final int DEFAULT_TABLE_SPACING = DEFAULT_FONT_SIZE;
+
+ // html stylesheet classnames for components
+ public static final String MESSAGE_CLASSNAME = "message";
+ public static final String SUBSECTION_CLASSNAME = "subsection";
+ public static final String SUBHEADER_CLASSNAME = "subheader";
+ public static final String SECTION_CLASSNAME = "section";
+ public static final String HEADER_CLASSNAME = "header";
+ public static final String VERTICAL_TABLE_CLASSNAME = "vertical-table";
+
+ // describing table values for a tag
+ private static final List> TAG_ENTRIES = Arrays.asList(
+ new ItemEntry<>(Bundle.Annotations_tagEntryDataLabel_tag(),
+ (tag) -> (tag.getName() != null) ? tag.getName().getDisplayName() : null),
+ new ItemEntry<>(Bundle.Annotations_tagEntryDataLabel_tagUser(), (tag) -> tag.getUserName()),
+ new ItemEntry<>(Bundle.Annotations_tagEntryDataLabel_comment(), (tag) -> tag.getComment())
+ );
+
+ private static final SectionConfig TAG_CONFIG
+ = new SectionConfig<>(Bundle.Annotations_tagEntry_title(), TAG_ENTRIES);
+
+ // file set attributes and table configurations
+ private static final List> FILESET_HIT_ENTRIES = Arrays.asList(
+ new ItemEntry<>(Bundle.Annotations_fileHitEntry_setName(),
+ (bba) -> tryGetAttribute(bba, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME)),
+ new ItemEntry<>(Bundle.Annotations_fileHitEntry_comment(),
+ (bba) -> tryGetAttribute(bba, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COMMENT))
+ );
+
+ private static final SectionConfig INTERESTING_FILE_CONFIG
+ = new SectionConfig<>(Bundle.Annotations_fileHitEntry_interestingFileHitTitle(), FILESET_HIT_ENTRIES);
+
+ private static final SectionConfig HASHSET_CONFIG
+ = new SectionConfig<>(Bundle.Annotations_fileHitEntry_hashSetHitTitle(), FILESET_HIT_ENTRIES);
+
+ private static final SectionConfig ARTIFACT_COMMENT_CONFIG
+ = new SectionConfig<>(Bundle.Annotations_fileHitEntry_artifactCommentTitle(), FILESET_HIT_ENTRIES);
+
+ // central repository attributes and table configuration
+ private static final List> CR_COMMENTS_ENTRIES = Arrays.asList(
+ new ItemEntry<>(Bundle.Annotations_centralRepositoryEntryDataLabel_case(),
+ cai -> (cai.getCorrelationCase() != null) ? cai.getCorrelationCase().getDisplayName() : null),
+ new ItemEntry<>(Bundle.Annotations_centralRepositoryEntryDataLabel_comment(), cai -> cai.getComment()),
+ new ItemEntry<>(Bundle.Annotations_centralRepositoryEntryDataLabel_path(), cai -> cai.getFilePath())
+ );
+
+ private static final SectionConfig CR_COMMENTS_CONFIG
+ = new SectionConfig<>(Bundle.Annotations_centralRepositoryEntry_title(), CR_COMMENTS_ENTRIES);
+
+ /*
+ * Private constructor for this utility class.
+ */
+ private Annotations() {
+
+ }
+
+ /**
+ * Returns the formatted Annotation information for the given node. If no
+ * data was found the method will return null;
+ *
+ * @param node Node to get data for.
+ *
+ * @return A formatted document of annotation information for the given node
+ * or null.
+ */
+ public static Document buildDocument(Node node) {
+ Document html = Jsoup.parse(EMPTY_HTML);
+ Element body = html.getElementsByTag("body").first();
+
+ BlackboardArtifact artifact = node.getLookup().lookup(BlackboardArtifact.class);
+ Content sourceFile = null;
+
+ try {
+ if (artifact != null) {
+ /*
+ * Get the source content based on the artifact to ensure we
+ * display the correct data instead of whatever was in the node.
+ */
+ sourceFile = artifact.getSleuthkitCase().getAbstractFileById(artifact.getObjectID());
+ } else {
+ /*
+ * No artifact is present, so get the content based on what's
+ * present in the node. In this case, the selected item IS the
+ * source file.
+ */
+ sourceFile = node.getLookup().lookup(AbstractFile.class);
+ }
+ } catch (TskCoreException ex) {
+ logger.log(Level.SEVERE, String.format(
+ "Exception while trying to retrieve a Content instance from the BlackboardArtifact '%s' (id=%d).",
+ artifact.getDisplayName(), artifact.getArtifactID()), ex);
+ }
+
+ boolean somethingWasRendered = false;
+ if (artifact != null) {
+ somethingWasRendered = renderArtifact(body, artifact, sourceFile);
+ } else {
+ somethingWasRendered = renderContent(body, sourceFile, false);
+ }
+
+ if (!somethingWasRendered) {
+ return null;
+ }
+
+ return html;
+ }
+
+ /**
+ * Renders annotations for an artifact.
+ *
+ * @param parent The html element to render content int.
+ * @param bba The blackboard artifact to render.
+ * @param sourceContent The content from which the blackboard artifact
+ * comes.
+ *
+ * @return If any content was actually rendered.
+ */
+ private static boolean renderArtifact(Element parent, BlackboardArtifact bba, Content sourceContent) {
+ boolean contentRendered = appendEntries(parent, TAG_CONFIG, getTags(bba), false);
+
+ if (CentralRepository.isEnabled()) {
+ List centralRepoComments = getCentralRepositoryData(bba);
+ boolean crRendered = appendEntries(parent, CR_COMMENTS_CONFIG, centralRepoComments, false);
+ contentRendered = contentRendered || crRendered;
+ }
+
+ // if artifact is a hashset hit or interesting file and has a non-blank comment
+ if ((BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID() == bba.getArtifactTypeID()
+ || BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getTypeID() == bba.getArtifactTypeID())
+ && (hasTskComment(bba))) {
+
+ boolean filesetRendered = appendEntries(parent, ARTIFACT_COMMENT_CONFIG, Arrays.asList(bba), false);
+ contentRendered = contentRendered || filesetRendered;
+ }
+
+ Element sourceFileSection = appendSection(parent, Bundle.Annotations_sourceFile_title());
+ boolean sourceFileRendered = renderContent(sourceFileSection, sourceContent, true);
+
+ if (!sourceFileRendered) {
+ sourceFileSection.remove();
+ }
+
+ return contentRendered || sourceFileRendered;
+ }
+
+ /**
+ * Renders annotations for a content item.
+ *
+ * @param parent The parent within which to render.
+ * @param sourceContent The content for which annotations will be gathered.
+ * @param isSubheader True if this section should be rendered as a
+ * subheader as opposed to a top-level header.
+ *
+ * @return If any content was actually rendered.
+ */
+ private static boolean renderContent(Element parent, Content sourceContent, boolean isSubheader) {
+ boolean contentRendered = appendEntries(parent, TAG_CONFIG, getTags(sourceContent), isSubheader);
+
+ if (sourceContent instanceof AbstractFile) {
+ AbstractFile sourceFile = (AbstractFile) sourceContent;
+
+ if (CentralRepository.isEnabled()) {
+ List centralRepoComments = getCentralRepositoryData(sourceFile);
+ boolean crRendered = appendEntries(parent, CR_COMMENTS_CONFIG, centralRepoComments, isSubheader);
+ contentRendered = contentRendered || crRendered;
+ }
+
+ boolean hashsetRendered = appendEntries(parent, HASHSET_CONFIG,
+ getFileSetHits(sourceFile, BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT),
+ isSubheader);
+
+ boolean interestingFileRendered = appendEntries(parent, INTERESTING_FILE_CONFIG,
+ getFileSetHits(sourceFile, BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT),
+ isSubheader);
+
+ contentRendered = contentRendered || hashsetRendered || interestingFileRendered;
+ }
+ return contentRendered;
+ }
+
+ /**
+ * Retrieves tags associated with a content item.
+ *
+ * @param sourceContent The content for which to gather content.
+ *
+ * @return The Tags associated with this item.
+ */
+ private static List getTags(Content sourceContent) {
+ try {
+ SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase();
+ return tskCase.getContentTagsByContent(sourceContent);
+ } catch (NoCurrentCaseException ex) {
+ logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS
+ } catch (TskCoreException ex) {
+ logger.log(Level.SEVERE, "Exception while getting tags from the case database.", ex); //NON-NLS
+ }
+ return new ArrayList<>();
+ }
+
+ /**
+ * Retrieves tags for blackboard artifact tags.
+ *
+ * @param bba The blackboard artifact for which to retrieve tags.
+ *
+ * @return The found tags.
+ */
+ private static List getTags(BlackboardArtifact bba) {
+ try {
+ SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase();
+ return tskCase.getBlackboardArtifactTagsByArtifact(bba);
+ } catch (NoCurrentCaseException ex) {
+ logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS
+ } catch (TskCoreException ex) {
+ logger.log(Level.SEVERE, "Exception while getting tags from the case database.", ex); //NON-NLS
+ }
+ return new ArrayList<>();
+ }
+
+ /**
+ * Retrieves the blackboard artifacts for a source file matching a certain
+ * type that have a non-blank TSK_COMMENT.
+ *
+ * @param sourceFile The source file for which to fetch artifacts.
+ * @param type The type of blackboard artifact to fetch.
+ *
+ * @return The artifacts found matching this type.
+ */
+ private static List getFileSetHits(AbstractFile sourceFile, BlackboardArtifact.ARTIFACT_TYPE type) {
+ try {
+ SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase();
+ return tskCase.getBlackboardArtifacts(type, sourceFile.getId()).stream()
+ .filter((bba) -> hasTskComment(bba))
+ .collect(Collectors.toList());
+ } catch (NoCurrentCaseException ex) {
+ logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS
+ } catch (TskCoreException ex) {
+ logger.log(Level.SEVERE, "Exception while getting file set hits from the case database.", ex); //NON-NLS
+ }
+ return new ArrayList<>();
+ }
+
+ /**
+ * Returns true if the artifact contains a non-blank TSK_COMMENT attribute.
+ *
+ * @param artifact The artifact to check.
+ *
+ * @return True if it has a non-blank TSK_COMMENT.
+ */
+ private static boolean hasTskComment(BlackboardArtifact artifact) {
+ return StringUtils.isNotBlank(tryGetAttribute(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COMMENT));
+ }
+
+ /**
+ * Attempts to retrieve the attribute of a particular type from a blackboard
+ * artifact.
+ *
+ * @param artifact The artifact from which to retrieve the information.
+ * @param attributeType The attribute type to retrieve from the artifact.
+ *
+ * @return The string value of the attribute or null if not found.
+ */
+ private static String tryGetAttribute(BlackboardArtifact artifact, BlackboardAttribute.ATTRIBUTE_TYPE attributeType) {
+ if (artifact == null) {
+ return null;
+ }
+
+ BlackboardAttribute attr = null;
+ try {
+ attr = artifact.getAttribute(new BlackboardAttribute.Type(attributeType));
+ } catch (TskCoreException ex) {
+ logger.log(Level.WARNING, String.format("Unable to fetch attribute of type %s for artifact %s", attributeType, artifact), ex);
+ }
+
+ if (attr == null) {
+ return null;
+ }
+
+ return attr.getValueString();
+ }
+
+ /**
+ * Gets the "Central Repository Comments" section with data for the
+ * blackboard artifact.
+ *
+ * @param artifact The selected artifact.
+ *
+ * @return The Correlation Attribute Instances associated with the artifact
+ * that have comments.
+ */
+ private static List getCentralRepositoryData(BlackboardArtifact artifact) {
+ if (artifact == null) {
+ return new ArrayList<>();
+ }
+
+ List> lookupKeys = CorrelationAttributeUtil.makeCorrAttrsForCorrelation(artifact)
+ .stream()
+ .map(cai -> Pair.of(cai.getCorrelationType(), cai.getCorrelationValue()))
+ .collect(Collectors.toList());
+
+ return getCorrelationAttributeComments(lookupKeys);
+ }
+
+ /**
+ * Gets the "Central Repository Comments" section with data.
+ *
+ * @param sourceFile A selected file, or a source file of the selected
+ * artifact.
+ *
+ * @return The Correlation Attribute Instances associated with the
+ * sourcefile that have comments.
+ */
+ private static List getCentralRepositoryData(AbstractFile sourceFile) {
+ if (sourceFile == null || StringUtils.isEmpty(sourceFile.getMd5Hash())) {
+ return new ArrayList<>();
+ }
+
+ List artifactTypes = null;
+ try {
+ artifactTypes = CentralRepository.getInstance().getDefinedCorrelationTypes();
+ } catch (CentralRepoException ex) {
+ logger.log(Level.SEVERE, "Error connecting to the Central Repository database.", ex); // NON-NLS
+ }
+
+ if (artifactTypes == null || artifactTypes.isEmpty()) {
+ return new ArrayList<>();
+ }
+
+ String md5 = sourceFile.getMd5Hash();
+
+ // get key lookups for a file attribute types and the md5 hash
+ List> lookupKeys = artifactTypes.stream()
+ .filter((attributeType) -> attributeType.getId() == CorrelationAttributeInstance.FILES_TYPE_ID)
+ .map((attributeType) -> Pair.of(attributeType, md5))
+ .collect(Collectors.toList());
+
+ return getCorrelationAttributeComments(lookupKeys);
+ }
+
+ /**
+ * Given a type and a value for that type, does a lookup in the Central
+ * Repository for matching values that have comments.
+ *
+ * @param lookupKeys The type and value to lookup.
+ *
+ * @return The found correlation attribute instances.
+ */
+ private static List getCorrelationAttributeComments(List> lookupKeys) {
+ List instancesToRet = new ArrayList<>();
+
+ try {
+ // use lookup instances to find the actual correlation attributes for the items selected
+ for (Pair typeVal : lookupKeys) {
+ instancesToRet.addAll(CentralRepository.getInstance()
+ .getArtifactInstancesByTypeValue(typeVal.getKey(), typeVal.getValue())
+ .stream()
+ // for each one found, if it has a comment, return
+ .filter((cai) -> StringUtils.isNotBlank(cai.getComment()))
+ .collect(Collectors.toList()));
+ }
+
+ } catch (CentralRepoException ex) {
+ logger.log(Level.SEVERE, "Error connecting to the Central Repository database.", ex); // NON-NLS
+ } catch (CorrelationAttributeNormalizationException ex) {
+ logger.log(Level.SEVERE, "Error normalizing instance from Central Repository database.", ex); // NON-NLS
+ }
+
+ return instancesToRet;
+ }
+
+ /**
+ * Append entries to the parent element in the annotations viewer. Entries
+ * will be formatted as a table in the format specified in the
+ * SectionConfig.
+ *
+ * @param parent The parent element for which the entries will be
+ * attached.
+ * @param config The display configuration for this entry type (i.e.
+ * table type, name, if data is not present).
+ * @param items The items to display.
+ * @param isSubsection Whether or not this should be displayed as a
+ * subsection. If not displayed as a top-level section.
+ *
+ * @return If there was actual content rendered for this set of entries.
+ */
+ private static boolean appendEntries(Element parent, Annotations.SectionConfig config, List extends T> items,
+ boolean isSubsection) {
+ if (items == null || items.isEmpty()) {
+ return false;
+ }
+
+ Element sectionDiv = (isSubsection) ? appendSubsection(parent, config.getTitle()) : appendSection(parent, config.getTitle());
+ appendVerticalEntryTables(sectionDiv, items, config.getAttributes());
+ return true;
+ }
+
+ /**
+ * Appends a table where items are displayed in rows of key-value pairs.
+ *
+ * @param parent The parent to append the table.
+ * @param items The items to process into a series of tables.
+ * @param rowHeaders The keys and the means to process items in order to get
+ * key-value pairs.
+ *
+ * @return The parent element provided as parameter.
+ */
+ private static Element appendVerticalEntryTables(Element parent, List extends T> items, List> rowHeaders) {
+ boolean isFirst = true;
+ for (T item : items) {
+ if (item == null) {
+ continue;
+ }
+
+ List> tableData = rowHeaders.stream()
+ .map(row -> Arrays.asList(row.getItemName(), row.retrieveValue(item)))
+ .collect(Collectors.toList());
+
+ Element childTable = appendTable(parent, 2, tableData, null);
+ childTable.attr("class", VERTICAL_TABLE_CLASSNAME);
+
+ if (isFirst) {
+ isFirst = false;
+ } else {
+ childTable.attr("style", String.format("margin-top: %dpx;", DEFAULT_TABLE_SPACING));
+ }
+ }
+
+ return parent;
+ }
+
+ /**
+ * Appends a generic table to the parent element.
+ *
+ * @param parent The parent element that will have a table appended
+ * to it.
+ * @param columnNumber The number of columns to append.
+ * @param content The content in content.get(row).get(column) format.
+ * @param columnHeaders The column headers or null if no column headers
+ * should be created.
+ *
+ * @return The created table.
+ */
+ private static Element appendTable(Element parent, int columnNumber, List> content, List columnHeaders) {
+ Element table = parent.appendElement("table");
+ if (columnHeaders != null && !columnHeaders.isEmpty()) {
+ Element header = table.appendElement("thead");
+ appendRow(header, columnHeaders, columnNumber, true);
+ }
+ Element tableBody = table.appendElement("tbody");
+
+ content.forEach((rowData) -> appendRow(tableBody, rowData, columnNumber, false));
+ return table;
+ }
+
+ /**
+ * Appends a row to the parent element (should be thead or tbody).
+ *
+ * @param rowParent The parent table element.
+ * @param data The data to place in columns within the table.
+ * @param columnNumber The number of columns to append.
+ * @param isHeader Whether or not this should have header cells ('th')
+ * instead of regular cells ('td').
+ *
+ * @return The row created.
+ */
+ private static Element appendRow(Element rowParent, List data, int columnNumber, boolean isHeader) {
+ String cellType = isHeader ? "th" : "td";
+ Element row = rowParent.appendElement("tr");
+ for (int i = 0; i < columnNumber; i++) {
+ Element cell = row.appendElement(cellType);
+ if (data != null && i < data.size()) {
+ cell.text(StringUtils.isEmpty(data.get(i)) ? "" : data.get(i));
+ }
+ }
+ return row;
+ }
+
+ /**
+ * Appends a new section with a section header to the parent element.
+ *
+ * @param parent The element to append this section to.
+ * @param headerText The text for the section.
+ *
+ * @return The div for the new section.
+ */
+ private static Element appendSection(Element parent, String headerText) {
+ Element sectionDiv = parent.appendElement("div");
+ sectionDiv.attr("class", SECTION_CLASSNAME);
+ Element header = sectionDiv.appendElement("h1");
+ header.text(headerText);
+ header.attr("class", HEADER_CLASSNAME);
+ return sectionDiv;
+ }
+
+ /**
+ * Appends a new subsection with a subsection header to the parent element.
+ *
+ * @param parent The element to append this subsection to.
+ * @param headerText The text for the subsection.
+ *
+ * @return The div for the new subsection.
+ */
+ private static Element appendSubsection(Element parent, String headerText) {
+ Element subsectionDiv = parent.appendElement("div");
+ subsectionDiv.attr("class", SUBSECTION_CLASSNAME);
+ Element header = subsectionDiv.appendElement("h2");
+ header.text(headerText);
+ header.attr("class", SUBHEADER_CLASSNAME);
+ return subsectionDiv;
+ }
+
+ /**
+ * Appends a message to the parent element. This is typically used in the
+ * event that no data exists for a certain type.
+ *
+ * @param parent The parent element that will have this message appended to
+ * it.
+ * @param message The message to append.
+ *
+ * @return The paragraph element for the new message.
+ */
+ private static Element appendMessage(Element parent, String message) {
+ Element messageEl = parent.appendElement("p");
+ messageEl.text(message);
+ messageEl.attr("class", MESSAGE_CLASSNAME);
+ return messageEl;
+ }
+
+ /**
+ * Describes a key value pair for an item of type T where the key is the
+ * field name to display and the value is retrieved from item of type T
+ * using a provided Function.
+ *
+ * @param The item type.
+ */
+ static class ItemEntry {
+
+ private final String itemName;
+ private final Function valueRetriever;
+
+ ItemEntry(String itemName, Function valueRetriever) {
+ this.itemName = itemName;
+ this.valueRetriever = valueRetriever;
+ }
+
+ String getItemName() {
+ return itemName;
+ }
+
+ Function getValueRetriever() {
+ return valueRetriever;
+ }
+
+ String retrieveValue(T object) {
+ return valueRetriever.apply(object);
+ }
+ }
+
+ /**
+ * Describes a section that will be appended to the annotations view panel.
+ *
+ * @param The item type for items to display.
+ */
+ static class SectionConfig {
+
+ private final String title;
+ private final List> attributes;
+
+ SectionConfig(String title, List> attributes) {
+ this.title = title;
+ this.attributes = attributes;
+ }
+
+ /**
+ * @return The title for the section.
+ */
+ String getTitle() {
+ return title;
+ }
+
+ /**
+ * @return Describes key-value pairs on the object to display to the
+ * user.
+ */
+ List> getAttributes() {
+ return attributes;
+ }
+ }
+
+}
diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/application/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/contentviewers/application/Bundle.properties-MERGED
new file mode 100755
index 0000000000..238576e0c7
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/contentviewers/application/Bundle.properties-MERGED
@@ -0,0 +1,18 @@
+Annotations.centralRepositoryEntry.title=Central Repository Comments
+Annotations.centralRepositoryEntryDataLabel.case=Case:
+Annotations.centralRepositoryEntryDataLabel.comment=Comment:
+Annotations.centralRepositoryEntryDataLabel.path=Path:
+Annotations.centralRepositoryEntryDataLabel.type=Type:
+Annotations.fileHitEntry.artifactCommentTitle=Artifact Comment
+Annotations.fileHitEntry.comment=Comment:
+Annotations.fileHitEntry.hashSetHitTitle=Hash Set Hit Comments
+Annotations.fileHitEntry.interestingFileHitTitle=Interesting File Hit Comments
+Annotations.fileHitEntry.setName=Set Name:
+Annotations.onEmpty=No annotations were found for this particular item.
+Annotations.sourceFile.title=Source File
+Annotations.tagEntry.title=Tags
+Annotations.tagEntryDataLabel.comment=Comment:
+Annotations.tagEntryDataLabel.tag=Tag:
+Annotations.tagEntryDataLabel.tagUser=Examiner:
+Annotations.title=Annotations
+Annotations.toolTip=Displays tags and comments associated with the selected content.