diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/AnnotationsContentViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/AnnotationsContentViewer.java index c87178bade..907538cc72 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/AnnotationsContentViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/AnnotationsContentViewer.java @@ -20,8 +20,14 @@ package org.sleuthkit.autopsy.contentviewers; import java.awt.Component; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +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.StringEscapeUtils; @@ -48,6 +54,10 @@ 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.jsoup.nodes.Document; +import org.jsoup.nodes.Element; + /** * Annotations view of file contents. @@ -77,7 +87,8 @@ public class AnnotationsContentViewer extends javax.swing.JPanel implements Data return; } - StringBuilder html = new StringBuilder(); + Document html = Jsoup.parse(EMPTY_HTML); + Element body = html.getElementsByTag("body").first(); BlackboardArtifact artifact = node.getLookup().lookup(BlackboardArtifact.class); Content sourceFile = null; @@ -104,20 +115,22 @@ public class AnnotationsContentViewer extends javax.swing.JPanel implements Data } if (artifact != null) { - populateTagData(html, artifact, sourceFile); + populateTagData(body, artifact, sourceFile); } else { - populateTagData(html, sourceFile); + populateTagData(body, sourceFile); } if (sourceFile instanceof AbstractFile) { - populateCentralRepositoryData(html, artifact, (AbstractFile) sourceFile); - populateFileSetData(html, sourceFile, BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT); - populateFileSetData(html, sourceFile, BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT); + populateCentralRepositoryData(body, artifact, (AbstractFile) sourceFile); + populateFileSetData(body, sourceFile, BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT); + populateFileSetData(body, sourceFile, BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT); } - setText(html.toString()); + jTextPane1.setText(html.html()); jTextPane1.setCaretPosition(0); } + + /** * Populates the html provided with data concerning the source file and @@ -285,9 +298,15 @@ public class AnnotationsContentViewer extends javax.swing.JPanel implements Data * @param sourceFile A selected file, or a source file of the selected * artifact. */ - private void populateCentralRepositoryData(StringBuilder html, BlackboardArtifact artifact, AbstractFile sourceFile) { + @NbBundle.Messages({ + "AnnotationsContentViewer.centralRepositoryEntryDataLabel.case=Case:", + "AnnotationsContentViewer.centralRepositoryEntryDataLabel.type=Type:", + "AnnotationsContentViewer.centralRepositoryEntryDataLabel.comment=Comment:", + "AnnotationsContentViewer.centralRepositoryEntryDataLabel.path=Path:" + }) + private List populateCentralRepositoryData(StringBuilder html, BlackboardArtifact artifact, AbstractFile sourceFile) { if (CentralRepository.isEnabled()) { - startSection(html, "Central Repository Comments"); + //startSection(html, "Central Repository Comments"); List instancesList = new ArrayList<>(); if (artifact != null) { instancesList.addAll(CorrelationAttributeUtil.makeCorrAttrsForCorrelation(artifact)); @@ -338,38 +357,6 @@ public class AnnotationsContentViewer extends javax.swing.JPanel implements Data } } - /** - * Set the text of the text panel. - * - * @param text The text to set to the text panel. - */ - private void setText(String text) { - jTextPane1.setText("" + text + ""); //NON-NLS - } - - /** - * Start a new data section. - * - * @param html The HTML text to add the section to. - * @param sectionName The name of the section. - */ - private void startSection(StringBuilder html, String sectionName) { - html.append("

") - .append(sectionName) - .append("


"); //NON-NLS - } - - /** - * Add a message. - * - * @param html The HTML text to add the message to. - * @param message The message text. - */ - private void addMessage(StringBuilder html, String message) { - html.append("

") - .append(message) - .append("


"); //NON-NLS - } /** * Add a data table containing information about a tag. @@ -390,83 +377,137 @@ public class AnnotationsContentViewer extends javax.swing.JPanel implements Data endTable(html); } - /** - * Add a data table containing information about a correlation attribute - * instance in the Central Repository. - * - * @param html The HTML text to add the table to. - * @param attributeInstance The attribute instance whose information will be - * used to populate the table. - */ - @NbBundle.Messages({ - "AnnotationsContentViewer.centralRepositoryEntryDataLabel.case=Case:", - "AnnotationsContentViewer.centralRepositoryEntryDataLabel.type=Type:", - "AnnotationsContentViewer.centralRepositoryEntryDataLabel.comment=Comment:", - "AnnotationsContentViewer.centralRepositoryEntryDataLabel.path=Path:" - }) - private void addCentralRepositoryEntry(StringBuilder html, CorrelationAttributeInstance attributeInstance) { - startTable(html); - addRow(html, Bundle.AnnotationsContentViewer_centralRepositoryEntryDataLabel_case(), attributeInstance.getCorrelationCase().getDisplayName()); - addRow(html, Bundle.AnnotationsContentViewer_centralRepositoryEntryDataLabel_type(), attributeInstance.getCorrelationType().getDisplayName()); - addRow(html, Bundle.AnnotationsContentViewer_centralRepositoryEntryDataLabel_comment(), formatHtmlString(attributeInstance.getComment())); - addRow(html, Bundle.AnnotationsContentViewer_centralRepositoryEntryDataLabel_path(), attributeInstance.getFilePath()); - endTable(html); + + private static final String EMPTY_HTML = ""; + + private static final int DEFAULT_FONT_SIZE = new JLabel().getFont().getSize(); + private static final int SUBHEADER_FONT_SIZE = DEFAULT_FONT_SIZE * 12 / 11; + private static final int HEADER_FONT_SIZE = DEFAULT_FONT_SIZE * 14 / 11; + + private static final String HEADER_STYLE = "font-size:" + HEADER_FONT_SIZE + "px;font-weight:bold;"; + private static final String SUBHEADER_STYLE = "font-size:" + SUBHEADER_FONT_SIZE + "px;font-weight:bold;"; + private static final String MESSAGE_STYLE = "font-size:" + DEFAULT_FONT_SIZE + "px;font-style:italic;"; + private static final String CONTENT_STYLE = "font-size:" + DEFAULT_FONT_SIZE + "px;"; + + private static final int DEFAULT_TABLE_SPACING = DEFAULT_FONT_SIZE * 2; + private static final int DEFAULT_SECTION_SPACING = DEFAULT_FONT_SIZE; + private static final int DEFAULT_SUBSECTION_SPACING = DEFAULT_FONT_SIZE; + + + private static final List> CENTRAL_REPO_COMMENTS = Arrays.asList( + new ColumnEntry<>(Bundle.AnnotationsContentViewer_centralRepositoryEntryDataLabel_case(), cai -> cai.getCorrelationCase().getDisplayName()), + new ColumnEntry<>(Bundle.AnnotationsContentViewer_centralRepositoryEntryDataLabel_type(), cai -> cai.getCorrelationType().getDisplayName()), + new ColumnEntry<>(Bundle.AnnotationsContentViewer_centralRepositoryEntryDataLabel_comment(), cai -> cai.getComment()), + new ColumnEntry<>(Bundle.AnnotationsContentViewer_centralRepositoryEntryDataLabel_path(), cai -> cai.getFilePath()) + ); + + private static class ColumnEntry { + private final String columnName; + private final Function valueRetriever; + + ColumnEntry(String columnName, Function valueRetriever) { + this.columnName = columnName; + this.valueRetriever = valueRetriever; + } + + String getColumnName() { + return columnName; + } + + Function getValueRetriever() { + return valueRetriever; + } + + String retrieveValue(T object) { + return valueRetriever.apply(object); + } + } + + private Element appendVerticalEntryTables(Element parent, List items, List> rowHeaders) { + items.stream() + .filter(item -> item != null) + .forEach((item) -> { + List> tableData = rowHeaders.stream() + .map(row -> Arrays.asList(row.getColumnName(), row.retrieveValue(item))) + .collect(Collectors.toList()); + + Element childTable = appendTable(parent, 2, tableData, null); + childTable.attr("style", String.format("margin-bottom: %dpx", DEFAULT_TABLE_SPACING)); + }); + + return parent; + } + + private Element appendEntryTable(Element parent, List items, List> columns) { + int columnNumber = columns.size(); + List columnHeaders = columns.stream().map(c -> c.getColumnName()).collect(Collectors.toList()); + List> rows = items.stream() + .filter(r -> r != null) + .map(r -> { + return columns.stream() + .map(c -> c.retrieveValue(r)) + .collect(Collectors.toList()); + }) + .collect(Collectors.toList()); + + Element table = appendTable(parent, columnNumber, rows, columnHeaders); + table.attr("style", String.format("margin-bottom: %dpx", DEFAULT_TABLE_SPACING)); + return table; + } + + + private Element appendTable(Element parent, int columnNumber, List> content, List columnHeaders) { + Element table = parent.appendElement("table"); + if (columnHeaders != null && !columnHeaders.isEmpty()) { + Element header = parent.appendElement("thead"); + appendRow(header, columnHeaders, columnNumber, true); + } + Element tableBody = table.appendElement("tbody"); + + content.forEach((rowData) -> appendRow(tableBody, rowData, columnNumber, false)); + return table; + } + + // TODO test sanitizing string + private 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.attr("valign", "top"); + cell.attr("style", CONTENT_STYLE); + cell.text(StringUtils.isEmpty(data.get(i)) ? "" : data.get(i)); + } + } + return row; + } + + private Element appendSection(Element parent, String headerText) { + Element parentDiv = parent.appendElement("div"); + parentDiv.attr("style", String.format("margin-bottom: %dpx;", DEFAULT_SECTION_SPACING)); + Element header = parentDiv.appendElement("h1"); + header.text(headerText); + header.attr("style", HEADER_STYLE); + return parentDiv; + } + + private Element appendSubsection(Element parent, String headerText) { + Element parentDiv = parent.appendElement("div"); + parentDiv.attr("style", String.format("margin-bottom: %dpx;", DEFAULT_SUBSECTION_SPACING)); + Element header = parentDiv.appendElement("h2"); + header.text(headerText); + header.attr("style", SUBHEADER_STYLE); + return parentDiv; + } + + private Element appendMessage(Element parent, String message) { + Element messageEl = parent.appendElement("p"); + messageEl.text(message); + messageEl.attr("style", MESSAGE_STYLE); + return messageEl; } - /** - * Start a data table. - * - * @param html The HTML text to add the table to. - */ - private void startTable(StringBuilder html) { - html.append(""); //NON-NLS - } - - /** - * Add a data row to a table. - * - * @param html The HTML text to add the row to. - * @param key The key for the left column of the data row. - * @param value The value for the right column of the data row. - */ - private void addRow(StringBuilder html, String key, String value) { - html.append(""); //NON-NLS - } - - /** - * End a data table. - * - * @param html The HTML text on which to end a table. - */ - private void endTable(StringBuilder html) { - html.append("
"); //NON-NLS - html.append(key); - html.append(""); //NON-NLS - html.append(value); - html.append("


"); //NON-NLS - } - - /** - * End a data section. - * - * @param html The HTML text on which to end a section. - */ - private void endSection(StringBuilder html) { - html.append("
"); //NON-NLS - } - - /** - * Apply escape sequence to special characters. Line feed and carriage - * return character combinations will be converted to HTML line breaks. - * - * @param text The text to format. - * - * @return The formatted text. - */ - private String formatHtmlString(String text) { - String formattedString = StringEscapeUtils.escapeHtml4(text); - return formattedString.replaceAll("(\r\n|\r|\n|\n\r)", "
"); - } /** * This method is called from within the constructor to initialize the form. @@ -554,6 +595,6 @@ public class AnnotationsContentViewer extends javax.swing.JPanel implements Data @Override public void resetComponent() { - setText(""); + jTextPane1.setText(EMPTY_HTML); } }