diff --git a/Core/src/org/sleuthkit/autopsy/communications/relationships/MessageNode.java b/Core/src/org/sleuthkit/autopsy/communications/relationships/MessageNode.java index e6ac3f507f..d1cdcbabf2 100755 --- a/Core/src/org/sleuthkit/autopsy/communications/relationships/MessageNode.java +++ b/Core/src/org/sleuthkit/autopsy/communications/relationships/MessageNode.java @@ -18,13 +18,10 @@ */ package org.sleuthkit.autopsy.communications.relationships; -import java.util.List; import java.util.TimeZone; import java.util.logging.Level; import org.apache.commons.lang3.StringUtils; import org.openide.nodes.Sheet; -import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; -import org.sleuthkit.autopsy.core.UserPreferences; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.datamodel.BlackboardArtifactNode; @@ -40,7 +37,6 @@ import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHO import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_TO; import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SUBJECT; import static org.sleuthkit.datamodel.BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DATETIME; -import org.sleuthkit.datamodel.Tag; import org.sleuthkit.datamodel.TimeUtilities; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.autopsy.communications.Utils; @@ -72,7 +68,6 @@ final class MessageNode extends BlackboardArtifactNode { @Override protected Sheet createSheet() { Sheet sheet = super.createSheet(); - List tags = getAllTagsFromDatabase(); Sheet.Set sheetSet = sheet.get(Sheet.PROPERTIES); if (sheetSet == null) { sheetSet = Sheet.createPropertiesSet(); @@ -81,17 +76,6 @@ final class MessageNode extends BlackboardArtifactNode { sheetSet.put(new NodeProperty<>("Type", Bundle.MessageNode_Node_Property_Type(), "", getDisplayName())); //NON-NLS - addScoreProperty(sheetSet, tags); - - CorrelationAttributeInstance correlationAttribute = null; - if (UserPreferences.hideCentralRepoCommentsAndOccurrences() == false) { - correlationAttribute = getCorrelationAttributeInstance(); - } - addCommentProperty(sheetSet, tags, correlationAttribute); - - if (UserPreferences.hideCentralRepoCommentsAndOccurrences() == false) { - addCountProperty(sheetSet, correlationAttribute); - } final BlackboardArtifact artifact = getArtifact(); BlackboardArtifact.ARTIFACT_TYPE fromID = BlackboardArtifact.ARTIFACT_TYPE.fromID(artifact.getArtifactTypeID()); diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/TextContentViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/TextContentViewer.java index 81384acbaa..d7f853caab 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/TextContentViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/TextContentViewer.java @@ -23,6 +23,7 @@ import org.openide.nodes.Node; import org.openide.util.NbBundle.Messages; import org.openide.util.lookup.ServiceProvider; import org.sleuthkit.autopsy.corecomponentinterfaces.DataContentViewer; +import org.sleuthkit.datamodel.AbstractFile; /** * A DataContentViewer that displays text with the TextViewers available. @@ -90,6 +91,17 @@ public class TextContentViewer implements DataContentViewer { if (node == null) { return false; } + // get the node's File, if it has one + AbstractFile file = node.getLookup().lookup(AbstractFile.class); + if (file == null) { + return false; + } + + // disable the text content viewer for directories and empty files + if (file.isDir() || file.getSize() == 0) { + return false; + } + return panel.isSupported(node); } diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/TextContentViewerPanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/TextContentViewerPanel.java index e1af060cfc..7c80d606fd 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/TextContentViewerPanel.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/TextContentViewerPanel.java @@ -79,7 +79,7 @@ public class TextContentViewerPanel extends javax.swing.JPanel implements DataCo } /** - * Deterime wether the content viewer which displays this panel isSupported. + * Determine whether the content viewer which displays this panel isSupported. * This panel is supported if any of the TextViewer's displayed in it are * supported. * diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties index 7eb261880f..fae74a30ac 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties @@ -217,3 +217,4 @@ DataResultViewerTable.pageNumLabel.text= DataResultViewerTable.pageLabel.text=Page: ViewPreferencesPanel.maxResultsLabel.text=Maximum number of Results to show in table: ViewPreferencesPanel.maxResultsLabel.toolTipText=\nSetting this value to 0 will display all results in the results table.\n
Note that setting this value to 0 may result in poor UI responsiveness when there are large numbers of results.\n +DataResultViewerTable.exportCSVButton.text=Save table as CSV diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED index cd75ae919e..b0e36da986 100755 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED @@ -32,6 +32,7 @@ DataResultViewerTable.commentRenderer.noComment.toolTip=No comments found DataResultViewerTable.commentRenderer.tagComment.toolTip=Comment exists on associated tag(s) DataResultViewerTable.countRender.name=O DataResultViewerTable.countRender.toolTip=O(ccurrences) indicates the number of data sources containing the item in the Central Repository +DataResultViewerTable.exportCSVButtonActionPerformed.empty=No data to export DataResultViewerTable.firstColLbl=Name DataResultViewerTable.goToPageTextField.err=Invalid page number # {0} - totalPages @@ -270,3 +271,4 @@ DataResultViewerTable.pageNumLabel.text= DataResultViewerTable.pageLabel.text=Page: ViewPreferencesPanel.maxResultsLabel.text=Maximum number of Results to show in table: ViewPreferencesPanel.maxResultsLabel.toolTipText=\nSetting this value to 0 will display all results in the results table.\n
Note that setting this value to 0 may result in poor UI responsiveness when there are large numbers of results.\n +DataResultViewerTable.exportCSVButton.text=Save table as CSV diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.form b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.form index 6b17cfd6dd..6aeda07298 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.form +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.form @@ -16,13 +16,13 @@ - - - + + + - + - + @@ -32,14 +32,15 @@ - + + - + @@ -48,9 +49,10 @@ + - - + + @@ -164,5 +166,15 @@ + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java index a5bbc461b8..e6c1110038 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java @@ -77,6 +77,7 @@ import org.openide.util.lookup.ServiceProvider; import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.corecomponentinterfaces.DataResultViewer; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.datamodel.NodeProperty; import org.sleuthkit.autopsy.datamodel.NodeSelectionInfo; @@ -176,6 +177,13 @@ public class DataResultViewerTable extends AbstractDataResultViewer { initComponents(); initializePagingSupport(); + + /* + * Disable the CSV export button for the common properties results + */ + if (this instanceof org.sleuthkit.autopsy.commonpropertiessearch.CommonAttributesSearchResultsViewerTable) { + exportCSVButton.setEnabled(false); + } /* * Configure the child OutlineView (explorer view) component. @@ -1291,6 +1299,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { outlineView = new OutlineView(DataResultViewerTable.FIRST_COLUMN_LABEL); gotoPageLabel = new javax.swing.JLabel(); gotoPageTextField = new javax.swing.JTextField(); + exportCSVButton = new javax.swing.JButton(); pageLabel.setText(org.openide.util.NbBundle.getMessage(DataResultViewerTable.class, "DataResultViewerTable.pageLabel.text")); // NOI18N @@ -1338,17 +1347,24 @@ public class DataResultViewerTable extends AbstractDataResultViewer { } }); + exportCSVButton.setText(org.openide.util.NbBundle.getMessage(DataResultViewerTable.class, "DataResultViewerTable.exportCSVButton.text")); // NOI18N + exportCSVButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + exportCSVButtonActionPerformed(evt); + } + }); + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(outlineView, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() - .addContainerGap(608, Short.MAX_VALUE) + .addComponent(outlineView, javax.swing.GroupLayout.DEFAULT_SIZE, 904, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addContainerGap() .addComponent(pageLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(pageNumLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 53, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGap(14, 14, 14) .addComponent(pagesLabel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) .addComponent(pagePrevButton, javax.swing.GroupLayout.PREFERRED_SIZE, 16, javax.swing.GroupLayout.PREFERRED_SIZE) @@ -1358,7 +1374,8 @@ public class DataResultViewerTable extends AbstractDataResultViewer { .addComponent(gotoPageLabel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(gotoPageTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 33, javax.swing.GroupLayout.PREFERRED_SIZE) - .addContainerGap()) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(exportCSVButton)) ); layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {pageNextButton, pagePrevButton}); @@ -1366,7 +1383,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() - .addContainerGap() + .addGap(3, 3, 3) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.CENTER) .addComponent(pageLabel) .addComponent(pageNumLabel) @@ -1374,9 +1391,10 @@ public class DataResultViewerTable extends AbstractDataResultViewer { .addComponent(pagePrevButton, javax.swing.GroupLayout.PREFERRED_SIZE, 14, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(pageNextButton, javax.swing.GroupLayout.PREFERRED_SIZE, 15, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(gotoPageLabel) - .addComponent(gotoPageTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(outlineView, javax.swing.GroupLayout.DEFAULT_SIZE, 324, Short.MAX_VALUE) + .addComponent(gotoPageTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(exportCSVButton)) + .addGap(3, 3, 3) + .addComponent(outlineView, javax.swing.GroupLayout.DEFAULT_SIZE, 321, Short.MAX_VALUE) .addContainerGap()) ); @@ -1397,7 +1415,19 @@ public class DataResultViewerTable extends AbstractDataResultViewer { pagingSupport.gotoPage(); }//GEN-LAST:event_gotoPageTextFieldActionPerformed + @NbBundle.Messages({"DataResultViewerTable.exportCSVButtonActionPerformed.empty=No data to export" + }) + private void exportCSVButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportCSVButtonActionPerformed + Node currentRoot = this.getExplorerManager().getRootContext(); + if (currentRoot != null && currentRoot.getChildren().getNodesCount() > 0) { + org.sleuthkit.autopsy.directorytree.ExportCSVAction.saveNodesToCSV(java.util.Arrays.asList(currentRoot.getChildren().getNodes()), this); + } else { + MessageNotifyUtil.Message.info(Bundle.DataResultViewerTable_exportCSVButtonActionPerformed_empty()); + } + }//GEN-LAST:event_exportCSVButtonActionPerformed + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton exportCSVButton; private javax.swing.JLabel gotoPageLabel; private javax.swing.JTextField gotoPageTextField; private org.openide.explorer.view.OutlineView outlineView; diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java index fa4529e44b..158f2ef4d9 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2018 Basis Technology Corp. + * Copyright 2011-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,6 @@ */ package org.sleuthkit.autopsy.datamodel; -import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.lang.ref.WeakReference; @@ -27,8 +26,6 @@ import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.logging.Level; import java.util.stream.Collectors; import org.apache.commons.io.FilenameUtils; @@ -65,6 +62,7 @@ import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.ContentTag; +import org.sleuthkit.datamodel.Tag; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; @@ -76,15 +74,10 @@ import org.sleuthkit.datamodel.TskData; public abstract class AbstractAbstractFileNode extends AbstractContentNode { private static final Logger logger = Logger.getLogger(AbstractAbstractFileNode.class.getName()); - @NbBundle.Messages("AbstractAbstractFileNode.addFileProperty.desc=no description") - private static final String NO_DESCR = AbstractAbstractFileNode_addFileProperty_desc(); private static final Set CASE_EVENTS_OF_INTEREST = EnumSet.of(Case.Events.CURRENT_CASE, Case.Events.CONTENT_TAG_ADDED, Case.Events.CONTENT_TAG_DELETED, Case.Events.CR_COMMENT_CHANGED); - private static final ExecutorService translationPool; - private static final Integer MAX_POOL_SIZE = 10; - /** * @param abstractFile file to wrap */ @@ -101,7 +94,7 @@ public abstract class AbstractAbstractFileNode extends A } if (UserPreferences.displayTranslatedFileNames()) { - AbstractAbstractFileNode.translationPool.submit(new TranslationTask( + backgroundTasksPool.submit(new TranslationTask( new WeakReference<>(this), weakPcl)); } @@ -109,14 +102,7 @@ public abstract class AbstractAbstractFileNode extends A // or when tags are added. Case.addEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, weakPcl); } - - static { - //Initialize this pool only once! This will be used by every instance of AAFN - //to do their heavy duty SCO column and translation updates. - translationPool = Executors.newFixedThreadPool(MAX_POOL_SIZE, - new ThreadFactoryBuilder().setNameFormat("translation-task-thread-%d").build()); - } - + /** * The finalizer removes event listeners as the BlackboardArtifactNode is * being garbage collected. Yes, we know that finalizers are considered to @@ -137,16 +123,7 @@ public abstract class AbstractAbstractFileNode extends A Case.removeEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, weakPcl); } - /** - * Event signals to indicate the background tasks have completed processing. - * Currently, we have one property task in the background: - * - * 1) Retreiving the translation of the file name - */ - enum NodeSpecificEvents { - TRANSLATION_AVAILABLE, - } - + private final PropertyChangeListener pcl = (PropertyChangeEvent evt) -> { String eventType = evt.getPropertyName(); @@ -190,7 +167,7 @@ public abstract class AbstractAbstractFileNode extends A } else if (eventType.equals(Case.Events.CONTENT_TAG_ADDED.toString())) { ContentTagAddedEvent event = (ContentTagAddedEvent) evt; if (event.getAddedTag().getContent().equals(content)) { - List tags = getContentTagsFromDatabase(); + List tags = this.getAllTagsFromDatabase(); Pair scorePropAndDescr = getScorePropertyAndDescription(tags); Score value = scorePropAndDescr.getLeft(); String descr = scorePropAndDescr.getRight(); @@ -202,7 +179,7 @@ public abstract class AbstractAbstractFileNode extends A } else if (eventType.equals(Case.Events.CONTENT_TAG_DELETED.toString())) { ContentTagDeletedEvent event = (ContentTagDeletedEvent) evt; if (event.getDeletedTagInfo().getContentID() == content.getId()) { - List tags = getContentTagsFromDatabase(); + List tags = getAllTagsFromDatabase(); Pair scorePropAndDescr = getScorePropertyAndDescription(tags); Score value = scorePropAndDescr.getLeft(); String descr = scorePropAndDescr.getRight(); @@ -214,7 +191,7 @@ public abstract class AbstractAbstractFileNode extends A } else if (eventType.equals(Case.Events.CR_COMMENT_CHANGED.toString())) { CommentChangedEvent event = (CommentChangedEvent) evt; if (event.getContentID() == content.getId()) { - List tags = getContentTagsFromDatabase(); + List tags = getAllTagsFromDatabase(); CorrelationAttributeInstance attribute = getCorrelationAttributeInstance(); updateSheet(new NodeProperty<>(COMMENT.toString(), COMMENT.toString(), NO_DESCR, getCommentProperty(tags, attribute))); } @@ -223,6 +200,18 @@ public abstract class AbstractAbstractFileNode extends A //Set the tooltip this.setShortDescription(content.getName()); updateSheet(new NodeProperty<>(ORIGINAL_NAME.toString(), ORIGINAL_NAME.toString(), NO_DESCR, content.getName())); + } else if (eventType.equals(NodeSpecificEvents.SCO_AVAILABLE.toString())) { + SCOData scoData = (SCOData)evt.getNewValue(); + if (scoData.getScoreAndDescription() != null) { + updateSheet(new NodeProperty<>(SCORE.toString(), SCORE.toString(), scoData.getScoreAndDescription().getRight(), scoData.getScoreAndDescription().getLeft())); + } + if (scoData.getComment() != null) { + updateSheet(new NodeProperty<>(COMMENT.toString(), COMMENT.toString(), NO_DESCR, scoData.getComment())); + } + if (scoData.getCountAndDescription() != null && + !UserPreferences.hideCentralRepoCommentsAndOccurrences()) { + updateSheet(new NodeProperty<>(OCCURRENCES.toString(), OCCURRENCES.toString(), scoData.getCountAndDescription().getRight(), scoData.getCountAndDescription().getLeft())); + } } }; /** @@ -235,38 +224,6 @@ public abstract class AbstractAbstractFileNode extends A */ private final PropertyChangeListener weakPcl = WeakListeners.propertyChange(pcl, null); - /** - * Updates the values of the properties in the current property sheet with - * the new properties being passed in. Only if that property exists in the - * current sheet will it be applied. That way, we allow for subclasses to - * add their own (or omit some!) properties and we will not accidentally - * disrupt their UI. - * - * Race condition if not synchronized. Only one update should be applied at - * a time. - * - * @param newProps New file property instances to be updated in the current - * sheet. - */ - private synchronized void updateSheet(NodeProperty... newProps) { - //Refresh ONLY those properties in the sheet currently. Subclasses may have - //only added a subset of our properties or their own props.s - Sheet visibleSheet = this.getSheet(); - Sheet.Set visibleSheetSet = visibleSheet.get(Sheet.PROPERTIES); - Property[] visibleProps = visibleSheetSet.getProperties(); - for (NodeProperty newProp : newProps) { - for (int i = 0; i < visibleProps.length; i++) { - if (visibleProps[i].getName().equals(newProp.getName())) { - visibleProps[i] = newProp; - } - } - } - visibleSheetSet.put(visibleProps); - visibleSheet.put(visibleSheetSet); - //setSheet() will notify Netbeans to update this node in the UI. - this.setSheet(visibleSheet); - } - /* * This is called when the node is first initialized. Any new updates or * changes happen by directly manipulating the sheet. That means we can fire @@ -368,18 +325,18 @@ public abstract class AbstractAbstractFileNode extends A properties.add(new NodeProperty<>(ORIGINAL_NAME.toString(), ORIGINAL_NAME.toString(), NO_DESCR, "")); } - //SCO column prereq info.. - List tags = getContentTagsFromDatabase(); - CorrelationAttributeInstance attribute = getCorrelationAttributeInstance(); - - Pair scoreAndDescription = getScorePropertyAndDescription(tags); - properties.add(new NodeProperty<>(SCORE.toString(), SCORE.toString(), scoreAndDescription.getRight(), scoreAndDescription.getLeft())); - DataResultViewerTable.HasCommentStatus comment = getCommentProperty(tags, attribute); - properties.add(new NodeProperty<>(COMMENT.toString(), COMMENT.toString(), NO_DESCR, comment)); - if (!UserPreferences.hideCentralRepoCommentsAndOccurrences()) { - Pair countAndDescription = getCountPropertyAndDescription(attribute); - properties.add(new NodeProperty<>(OCCURRENCES.toString(), OCCURRENCES.toString(), countAndDescription.getRight(), countAndDescription.getLeft())); + // Create place holders for S C O + properties.add(new NodeProperty<>(SCORE.toString(), SCORE.toString(), VALUE_LOADING, "")); + properties.add(new NodeProperty<>(COMMENT.toString(), COMMENT.toString(), VALUE_LOADING, "")); + if (UserPreferences.hideCentralRepoCommentsAndOccurrences() == false) { + properties.add(new NodeProperty<>(OCCURRENCES.toString(), OCCURRENCES.toString(), VALUE_LOADING, "")); } + + + // Get the SCO columns data in a background task + backgroundTasksPool.submit(new GetSCOTask( + new WeakReference<>(this), weakPcl)); + properties.add(new NodeProperty<>(LOCATION.toString(), LOCATION.toString(), NO_DESCR, getContentPath(content))); properties.add(new NodeProperty<>(MOD_TIME.toString(), MOD_TIME.toString(), NO_DESCR, ContentUtils.getStringTime(content.getMtime(), content))); properties.add(new NodeProperty<>(CHANGED_TIME.toString(), CHANGED_TIME.toString(), NO_DESCR, ContentUtils.getStringTime(content.getCtime(), content))); @@ -441,7 +398,8 @@ public abstract class AbstractAbstractFileNode extends A "AbstractAbstractFileNode.createSheet.count.hashLookupNotRun.description=Hash lookup had not been run on this file when the column was populated", "# {0} - occuranceCount", "AbstractAbstractFileNode.createSheet.count.description=There were {0} datasource(s) found with occurances of the correlation value"}) - Pair getCountPropertyAndDescription(CorrelationAttributeInstance attribute) { + @Override + protected Pair getCountPropertyAndDescription(CorrelationAttributeInstance attribute) { Long count = -1L; //The column renderer will not display negative values, negative value used when count unavailble to preserve sorting String description = Bundle.AbstractAbstractFileNode_createSheet_count_noCentralRepo_description(); try { @@ -468,7 +426,8 @@ public abstract class AbstractAbstractFileNode extends A "AbstractAbstractFileNode.createSheet.taggedFile.description=File has been tagged.", "AbstractAbstractFileNode.createSheet.notableTaggedFile.description=File tagged with notable tag.", "AbstractAbstractFileNode.createSheet.noScore.description=No score"}) - Pair getScorePropertyAndDescription(List tags) { + @Override + protected Pair getScorePropertyAndDescription(List tags) { DataResultViewerTable.Score score = DataResultViewerTable.Score.NO_SCORE; String description = Bundle.AbstractAbstractFileNode_createSheet_noScore_description(); if (content.getKnown() == TskData.FileKnown.BAD) { @@ -486,7 +445,7 @@ public abstract class AbstractAbstractFileNode extends A if (!tags.isEmpty() && (score == DataResultViewerTable.Score.NO_SCORE || score == DataResultViewerTable.Score.INTERESTING_SCORE)) { score = DataResultViewerTable.Score.INTERESTING_SCORE; description = Bundle.AbstractAbstractFileNode_createSheet_taggedFile_description(); - for (ContentTag tag : tags) { + for (Tag tag : tags) { if (tag.getName().getKnownStatus() == TskData.FileKnown.BAD) { score = DataResultViewerTable.Score.NOTABLE_SCORE; description = Bundle.AbstractAbstractFileNode_createSheet_notableTaggedFile_description(); @@ -499,11 +458,12 @@ public abstract class AbstractAbstractFileNode extends A @NbBundle.Messages({ "AbstractAbstractFileNode.createSheet.comment.displayName=C"}) - HasCommentStatus getCommentProperty(List tags, CorrelationAttributeInstance attribute) { + @Override + protected HasCommentStatus getCommentProperty(List tags, CorrelationAttributeInstance attribute) { DataResultViewerTable.HasCommentStatus status = !tags.isEmpty() ? DataResultViewerTable.HasCommentStatus.TAG_NO_COMMENT : DataResultViewerTable.HasCommentStatus.NO_COMMENT; - for (ContentTag tag : tags) { + for (Tag tag : tags) { if (!StringUtils.isBlank(tag.getComment())) { //if the tag is null or empty or contains just white space it will indicate there is not a comment status = DataResultViewerTable.HasCommentStatus.TAG_COMMENT; @@ -571,7 +531,13 @@ public abstract class AbstractAbstractFileNode extends A return tags; } - CorrelationAttributeInstance getCorrelationAttributeInstance() { + @Override + protected List getAllTagsFromDatabase() { + return new ArrayList<>(getContentTagsFromDatabase()); + } + + @Override + protected CorrelationAttributeInstance getCorrelationAttributeInstance() { CorrelationAttributeInstance attribute = null; if (EamDb.isEnabled() && !UserPreferences.hideCentralRepoCommentsAndOccurrences()) { attribute = EamArtifactUtil.getInstanceFromContent(content); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractContentNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractContentNode.java index 4f6f2e5f47..b9e012e6af 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractContentNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractContentNode.java @@ -18,20 +18,29 @@ */ package org.sleuthkit.autopsy.datamodel; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.logging.Level; +import org.apache.commons.lang3.tuple.Pair; import org.openide.nodes.Children; +import org.openide.nodes.Sheet; import org.openide.util.lookup.Lookups; import org.openide.util.Lookup; +import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; +import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.Tag; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; import org.sleuthkit.datamodel.TskException; @@ -50,6 +59,38 @@ public abstract class AbstractContentNode extends ContentNode T content; private static final Logger logger = Logger.getLogger(AbstractContentNode.class.getName()); + /** + * A pool of background tasks to run any long computation needed to + * populate this node. + */ + static final ExecutorService backgroundTasksPool; + private static final Integer MAX_POOL_SIZE = 10; + + /** + * Default no description string + */ + @NbBundle.Messages({"AbstractContentNode.nodescription=no description", + "AbstractContentNode.valueLoading=value loading"}) + protected static final String NO_DESCR = Bundle.AbstractContentNode_nodescription(); + protected static final String VALUE_LOADING = Bundle.AbstractContentNode_valueLoading(); + + /** + * Event signals to indicate the background tasks have completed processing. + * Currently, we have one property task in the background: + * + * 1) Retrieving the translation of the file name + */ + enum NodeSpecificEvents { + TRANSLATION_AVAILABLE, + SCO_AVAILABLE + } + + static { + //Initialize this pool only once! This will be used by every instance of AAFN + //to do their heavy duty SCO column and translation updates. + backgroundTasksPool = Executors.newFixedThreadPool(MAX_POOL_SIZE, + new ThreadFactoryBuilder().setNameFormat("content-node-background-task-%d").build()); + } /** * Handles aspects that depend on the Content object * @@ -240,4 +281,79 @@ public abstract class AbstractContentNode extends ContentNode public int read(byte[] buf, long offset, long len) throws TskException { return content.read(buf, offset, len); } + + + /** + * Updates the values of the properties in the current property sheet with + * the new properties being passed in. Only if that property exists in the + * current sheet will it be applied. That way, we allow for subclasses to + * add their own (or omit some!) properties and we will not accidentally + * disrupt their UI. + * + * Race condition if not synchronized. Only one update should be applied at + * a time. + * + * @param newProps New file property instances to be updated in the current + * sheet. + */ + protected synchronized void updateSheet(NodeProperty... newProps) { + //Refresh ONLY those properties in the sheet currently. Subclasses may have + //only added a subset of our properties or their own props.s + Sheet visibleSheet = this.getSheet(); + Sheet.Set visibleSheetSet = visibleSheet.get(Sheet.PROPERTIES); + Property[] visibleProps = visibleSheetSet.getProperties(); + for (NodeProperty newProp : newProps) { + for (int i = 0; i < visibleProps.length; i++) { + if (visibleProps[i].getName().equals(newProp.getName())) { + visibleProps[i] = newProp; + } + } + } + visibleSheetSet.put(visibleProps); + visibleSheet.put(visibleSheetSet); + //setSheet() will notify Netbeans to update this node in the UI. + this.setSheet(visibleSheet); + } + + /** + * Reads and returns a list of all tags associated with this content node. + * + * @return list of tags associated with the node. + */ + abstract protected List getAllTagsFromDatabase(); + + /** + * Returns correlation attribute instance for the underlying content of the node. + * + * @return correlation attribute instance for the underlying content of the node. + */ + abstract protected CorrelationAttributeInstance getCorrelationAttributeInstance(); + + /** + * Returns Score property for the node. + * + * @param tags list of tags. + * + * @return Score property for the underlying content of the node. + */ + abstract protected Pair getScorePropertyAndDescription(List tags); + + /** + * Returns comment property for the node. + * + * @param tags list of tags + * @param attribute correlation attribute instance + * + * @return Comment property for the underlying content of the node. + */ + abstract protected DataResultViewerTable.HasCommentStatus getCommentProperty(List tags, CorrelationAttributeInstance attribute); + + /** + * Returns occurrences/count property for the node. + * + * @param attribute correlation attribute instance + * + * @return count property for the underlying content of the node. + */ + abstract protected Pair getCountPropertyAndDescription(CorrelationAttributeInstance attribute); } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java index 55e81acd17..23ab78fa01 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2018 Basis Technology Corp. + * Copyright 2011-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +22,7 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.lang.ref.WeakReference; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; @@ -37,6 +38,7 @@ import java.util.logging.Level; import java.util.stream.Collectors; import javax.swing.Action; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; import org.openide.nodes.Sheet; import org.openide.util.Lookup; import org.openide.util.NbBundle; @@ -55,11 +57,13 @@ import org.sleuthkit.autopsy.centralrepository.datamodel.EamArtifactUtil; import org.sleuthkit.autopsy.centralrepository.datamodel.EamDb; import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable; import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable.Score; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import static org.sleuthkit.autopsy.datamodel.DisplayableItemNode.findLinked; import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable.HasCommentStatus; +import static org.sleuthkit.autopsy.datamodel.AbstractContentNode.backgroundTasksPool; import org.sleuthkit.autopsy.modules.hashdatabase.HashDbManager; import org.sleuthkit.autopsy.timeline.actions.ViewArtifactInTimelineAction; import org.sleuthkit.autopsy.timeline.actions.ViewFileInTimelineAction; @@ -95,10 +99,7 @@ public class BlackboardArtifactNode extends AbstractContentNode> customProperties; - - private final static String NO_DESCR = NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.noDesc.text"); - - + /* * Artifact types which should have the full unique path of the associated * content as a property. @@ -151,6 +152,19 @@ public class BlackboardArtifactNode extends AbstractContentNode(Bundle.BlackboardArtifactNode_createSheet_score_name(), Bundle.BlackboardArtifactNode_createSheet_score_displayName(), scoData.getScoreAndDescription().getRight(), scoData.getScoreAndDescription().getLeft())); + } + if (scoData.getComment() != null) { + updateSheet(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_comment_name(), Bundle.BlackboardArtifactNode_createSheet_comment_displayName(), NO_DESCR, scoData.getComment())); + } + if (scoData.getCountAndDescription() != null && + !UserPreferences.hideCentralRepoCommentsAndOccurrences()) { + updateSheet(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_count_name(), Bundle.BlackboardArtifactNode_createSheet_count_displayName(), scoData.getCountAndDescription().getRight(), scoData.getCountAndDescription().getLeft())); + } + } } }; @@ -335,8 +349,6 @@ public class BlackboardArtifactNode extends AbstractContentNode tags = getAllTagsFromDatabase(); - Sheet.Set sheetSet = sheet.get(Sheet.PROPERTIES); if (sheetSet == null) { sheetSet = Sheet.createPropertiesSet(); @@ -351,17 +363,17 @@ public class BlackboardArtifactNode extends AbstractContentNode(Bundle.BlackboardArtifactNode_createSheet_score_name(), Bundle.BlackboardArtifactNode_createSheet_score_displayName(), VALUE_LOADING, "")); + sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_comment_name(), Bundle.BlackboardArtifactNode_createSheet_comment_displayName(), VALUE_LOADING, "")); if (UserPreferences.hideCentralRepoCommentsAndOccurrences() == false) { - correlationAttribute = getCorrelationAttributeInstance(); - } - addCommentProperty(sheetSet, tags, correlationAttribute); - - if (UserPreferences.hideCentralRepoCommentsAndOccurrences() == false) { - addCountProperty(sheetSet, correlationAttribute); + sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_count_name(), Bundle.BlackboardArtifactNode_createSheet_count_displayName(), VALUE_LOADING, "")); } + + // Get the SCO columns data in a background task + backgroundTasksPool.submit(new GetSCOTask( + new WeakReference<>(this), weakPcl)); + if (artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_INTERESTING_ARTIFACT_HIT.getTypeID()) { try { BlackboardAttribute attribute = artifact.getAttribute(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT)); @@ -520,6 +532,7 @@ public class BlackboardArtifactNode extends AbstractContentNode getAllTagsFromDatabase() { List tags = new ArrayList<>(); try { @@ -569,6 +582,7 @@ public class BlackboardArtifactNode extends AbstractContentNode t.getName().getDisplayName()).collect(Collectors.joining(", ")))); } + @Override protected final CorrelationAttributeInstance getCorrelationAttributeInstance() { CorrelationAttributeInstance correlationAttribute = null; if (EamDb.isEnabled()) { @@ -581,16 +595,36 @@ public class BlackboardArtifactNode extends AbstractContentNode tags, CorrelationAttributeInstance attribute) { + HasCommentStatus status = getCommentProperty(tags, attribute ); + sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_comment_name(), Bundle.BlackboardArtifactNode_createSheet_comment_displayName(), NO_DESCR, + status)); + } + + /** + * Gets the comment property for the node + * + * @param tags the list of tags associated with the file + * @param attribute the correlation attribute associated with this + * artifact's associated file, null if central repo is not + * enabled + * @return comment property + */ + @Override + protected DataResultViewerTable.HasCommentStatus getCommentProperty(List tags, CorrelationAttributeInstance attribute) { + HasCommentStatus status = tags.size() > 0 ? HasCommentStatus.TAG_NO_COMMENT : HasCommentStatus.NO_COMMENT; for (Tag tag : tags) { if (!StringUtils.isBlank(tag.getComment())) { @@ -609,17 +643,17 @@ public class BlackboardArtifactNode extends AbstractContentNode(Bundle.BlackboardArtifactNode_createSheet_comment_name(), Bundle.BlackboardArtifactNode_createSheet_comment_displayName(), NO_DESCR, - status)); + return status; } - /** * Used by (subclasses of) BlackboardArtifactNode to add the Score property * to their sheets. * - * @param sheetSet the modifiable Sheet.Set returned by - * Sheet.get(Sheet.PROPERTIES) + * @param sheetSet the modifiable Sheet.Set to add the property to * @param tags the list of tags associated with the file + * + * @deprecated Use the GetSCOTask to get this data on a background thread..., + * and then update the property sheet asynchronously */ @NbBundle.Messages({"BlackboardArtifactNode.createSheet.score.name=S", "BlackboardArtifactNode.createSheet.score.displayName=S", @@ -628,8 +662,22 @@ public class BlackboardArtifactNode extends AbstractContentNode tags) { - Score score = Score.NO_SCORE; + @Deprecated + protected final void addScorePropertyAndDescription(Sheet.Set sheetSet, List tags) { + Pair scoreAndDescription = getScorePropertyAndDescription(tags); + sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_score_name(), Bundle.BlackboardArtifactNode_createSheet_score_displayName(), scoreAndDescription.getRight(), scoreAndDescription.getLeft())); + } + + /** + * Get the score property for the node. + * + * @param tags the list of tags associated with the file + * + * @return score property and description + */ + @Override + protected Pair getScorePropertyAndDescription(List tags) { + Score score = Score.NO_SCORE; String description = Bundle.BlackboardArtifactNode_createSheet_noScore_description(); if (associated instanceof AbstractFile) { if (((AbstractFile) associated).getKnown() == TskData.FileKnown.BAD) { @@ -673,18 +721,43 @@ public class BlackboardArtifactNode extends AbstractContentNode(Bundle.BlackboardArtifactNode_createSheet_score_name(), Bundle.BlackboardArtifactNode_createSheet_score_displayName(), description, score)); + + return Pair.of(score, description); } - + /** + * Used by (subclasses of) BlackboardArtifactNode to add the Occurrences property + * to their sheets. + * + * @param sheetSet the modifiable Sheet.Set to add the property to + * @param attribute correlation attribute instance + * + * @deprecated Use the GetSCOTask to get this data on a background thread..., + * and then update the property sheet asynchronously + */ @NbBundle.Messages({"BlackboardArtifactNode.createSheet.count.name=O", "BlackboardArtifactNode.createSheet.count.displayName=O", "BlackboardArtifactNode.createSheet.count.noCentralRepo.description=Central repository was not enabled when this column was populated", "BlackboardArtifactNode.createSheet.count.hashLookupNotRun.description=Hash lookup had not been run on this artifact's associated file when the column was populated", "# {0} - occuranceCount", "BlackboardArtifactNode.createSheet.count.description=There were {0} datasource(s) found with occurances of the correlation value"}) - + @Deprecated protected final void addCountProperty(Sheet.Set sheetSet, CorrelationAttributeInstance attribute) { - Long count = -1L; //The column renderer will not display negative values, negative value used when count unavailble to preserve sorting + Pair countAndDescription = getCountPropertyAndDescription(attribute); + sheetSet.put( + new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_count_name(), Bundle.BlackboardArtifactNode_createSheet_count_displayName(), countAndDescription.getRight(), countAndDescription.getLeft())); + } + + /** + * Gets the Occurrences property for the node. + * + * @param attribute correlation attribute instance + * + * @return count and description + * + */ + @Override + protected Pair getCountPropertyAndDescription(CorrelationAttributeInstance attribute) { + Long count = -1L; String description = Bundle.BlackboardArtifactNode_createSheet_count_noCentralRepo_description(); try { //don't perform the query if there is no correlation value @@ -699,14 +772,13 @@ public class BlackboardArtifactNode extends AbstractContentNode(Bundle.BlackboardArtifactNode_createSheet_count_name(), Bundle.BlackboardArtifactNode_createSheet_count_displayName(), description, count)); - } - + return Pair.of(count, description); + } + private void updateSheet() { this.setSheet(createSheet()); } - + private String getRootParentName() { String parentName = associated.getName(); Content parent = associated; diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED index 1d8fb7eb14..0386b0fef2 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED @@ -1,5 +1,4 @@ AbstractAbstractFileNode.accessTimeColLbl=Access Time -AbstractAbstractFileNode.addFileProperty.desc=no description AbstractAbstractFileNode.attrAddrColLbl=Attr. Addr. AbstractAbstractFileNode.changeTimeColLbl=Change Time AbstractAbstractFileNode.createdTimeColLbl=Created Time @@ -37,6 +36,8 @@ AbstractAbstractFileNode.tagsProperty.displayName=Tags AbstractAbstractFileNode.typeDirColLbl=Type(Dir) AbstractAbstractFileNode.typeMetaColLbl=Type(Meta) AbstractAbstractFileNode.useridColLbl=UserID +AbstractContentNode.nodescription=no description +AbstractContentNode.valueLoading=value loading AbstractFsContentNode.noDesc.text=no description ArtifactStringContent.attrsTableHeader.sources=Source(s) ArtifactStringContent.attrsTableHeader.type=Type diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DataModelActionsFactory.java b/Core/src/org/sleuthkit/autopsy/datamodel/DataModelActionsFactory.java index 13a0753ca1..bdb4a394a8 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DataModelActionsFactory.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DataModelActionsFactory.java @@ -36,6 +36,7 @@ import org.sleuthkit.autopsy.actions.ReplaceBlackboardArtifactTagAction; import org.sleuthkit.autopsy.actions.ReplaceContentTagAction; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; import org.sleuthkit.autopsy.datamodel.Reports.ReportNode; +import org.sleuthkit.autopsy.directorytree.ExportCSVAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerShortcutAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; @@ -92,6 +93,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -119,6 +121,7 @@ public class DataModelActionsFactory { actionsList.add(new NewWindowViewAction(VIEW_IN_NEW_WINDOW, slackFileNode)); actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -155,6 +158,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance());// + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -189,6 +193,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -223,6 +228,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -257,6 +263,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -291,6 +298,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -325,6 +333,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -379,6 +388,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -415,6 +425,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DirectoryNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/DirectoryNode.java index 97b0fd8216..5a3476f896 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DirectoryNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DirectoryNode.java @@ -28,6 +28,7 @@ import org.openide.util.Utilities; import org.sleuthkit.autopsy.actions.AddContentTagAction; import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; +import org.sleuthkit.autopsy.directorytree.ExportCSVAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; import org.sleuthkit.autopsy.directorytree.NewWindowViewAction; import org.sleuthkit.autopsy.directorytree.ViewContextAction; @@ -89,6 +90,7 @@ public class DirectoryNode extends AbstractFsContentNode { actionsList.add(ViewFileInTimelineAction.createViewFileAction(content)); actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(new RunIngestModulesAction(content)); actionsList.add(null); // creates a menu separator diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java index 1695b253b0..7b3a55c20b 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java @@ -32,6 +32,7 @@ import org.sleuthkit.autopsy.actions.AddContentTagAction; import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.directorytree.ExportCSVAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerShortcutAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; @@ -173,6 +174,7 @@ public class FileNode extends AbstractFsContentNode { actionsList.add(null); // Creates an item separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // Creates an item separator actionsList.add(AddContentTagAction.getInstance()); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/GetSCOTask.java b/Core/src/org/sleuthkit/autopsy/datamodel/GetSCOTask.java new file mode 100644 index 0000000000..b7f4e38a5e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/GetSCOTask.java @@ -0,0 +1,73 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 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.datamodel; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.lang.ref.WeakReference; +import java.util.List; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; +import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.autopsy.events.AutopsyEvent; +import org.sleuthkit.datamodel.Tag; + +/** + * Background task to get Score, Comment and Occurrences values for an + * Abstract content node. + * + */ +class GetSCOTask implements Runnable { + + private final WeakReference> weakNodeRef; + private final PropertyChangeListener listener; + + GetSCOTask(WeakReference> weakContentRef, PropertyChangeListener listener) { + this.weakNodeRef = weakContentRef; + this.listener = listener; + } + + @Override + public void run() { + AbstractContentNode contentNode = weakNodeRef.get(); + + //Check for stale reference + if (contentNode == null) { + return; + } + + // get the SCO column values + List tags = contentNode.getAllTagsFromDatabase(); + CorrelationAttributeInstance attribute = contentNode.getCorrelationAttributeInstance(); + + SCOData scoData = new SCOData(); + scoData.setScoreAndDescription(contentNode.getScorePropertyAndDescription(tags)); + scoData.setComment(contentNode.getCommentProperty(tags, attribute)); + if (!UserPreferences.hideCentralRepoCommentsAndOccurrences()) { + scoData.setCountAndDescription(contentNode.getCountPropertyAndDescription(attribute)); + } + + // signal SCO data is available. + if (listener != null) { + listener.propertyChange(new PropertyChangeEvent( + AutopsyEvent.SourceType.LOCAL.toString(), + AbstractAbstractFileNode.NodeSpecificEvents.SCO_AVAILABLE.toString(), + null, scoData)); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ImageNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/ImageNode.java index 584a405fcc..b2c80f1322 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/ImageNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ImageNode.java @@ -28,12 +28,15 @@ import java.util.EnumSet; import java.util.List; import java.util.logging.Level; import javax.swing.Action; +import org.apache.commons.lang3.tuple.Pair; import org.openide.nodes.Sheet; import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.casemodule.datasourcesummary.ViewSummaryInformationAction; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; +import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.directorytree.ExplorerNodeActionVisitor; import org.sleuthkit.autopsy.directorytree.FileSearchAction; @@ -47,6 +50,7 @@ import org.sleuthkit.datamodel.SleuthkitCase.CaseDbQuery; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.VirtualDirectory; import org.sleuthkit.autopsy.datamodel.BaseChildFactory.NoSuchEventBusException; +import org.sleuthkit.datamodel.Tag; /** * This class is used to represent the "Node" for the image. The children of @@ -250,4 +254,67 @@ public class ImageNode extends AbstractContentNode { } }; + /** + * Reads and returns a list of all tags associated with this content node. + * + * Null implementation of an abstract method. + * + * @return list of tags associated with the node. + */ + @Override + protected List getAllTagsFromDatabase() { + return new ArrayList<>(); + } + /** + * Returns correlation attribute instance for the underlying content of the node. + * + * Null implementation of an abstract method. + * + * @return correlation attribute instance for the underlying content of the node. + */ + @Override + protected CorrelationAttributeInstance getCorrelationAttributeInstance() { + return null; + } + + /** + * Returns Score property for the node. + * + * Null implementation of an abstract method. + * + * @param tags list of tags. + * + * @return Score property for the underlying content of the node. + */ + @Override + protected Pair getScorePropertyAndDescription(List tags) { + return Pair.of(DataResultViewerTable.Score.NO_SCORE, NO_DESCR); + } + /** + * Returns comment property for the node. + * + * Null implementation of an abstract method. + * + * @param tags list of tags + * @param attribute correlation attribute instance + * + * @return Comment property for the underlying content of the node. + */ + @Override + protected DataResultViewerTable.HasCommentStatus getCommentProperty(List tags, CorrelationAttributeInstance attribute) { + return DataResultViewerTable.HasCommentStatus.NO_COMMENT; + } + /** + * Returns occurrences/count property for the node. + * + * Null implementation of an abstract method. + * + * @param attribute correlation attribute instance + * + * @return count property for the underlying content of the node. + */ + @Override + protected Pair getCountPropertyAndDescription(CorrelationAttributeInstance attribute) { + return Pair.of(-1L, NO_DESCR); + } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/LayoutFileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/LayoutFileNode.java index bfd8f7bd91..41ecad0340 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/LayoutFileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/LayoutFileNode.java @@ -29,6 +29,7 @@ import org.openide.util.Utilities; import org.sleuthkit.autopsy.actions.AddContentTagAction; import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; +import org.sleuthkit.autopsy.directorytree.ExportCSVAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerShortcutAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; @@ -105,6 +106,7 @@ public class LayoutFileNode extends AbstractAbstractFileNode { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/LocalFileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/LocalFileNode.java index 6d64aeb621..4a62cd9e4f 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/LocalFileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/LocalFileNode.java @@ -31,6 +31,7 @@ import org.sleuthkit.autopsy.actions.AddContentTagAction; import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.directorytree.ExportCSVAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerShortcutAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; @@ -82,6 +83,7 @@ public class LocalFileNode extends AbstractAbstractFileNode { actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/SCOData.java b/Core/src/org/sleuthkit/autopsy/datamodel/SCOData.java new file mode 100644 index 0000000000..a9dd99369d --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/SCOData.java @@ -0,0 +1,56 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 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.datamodel; + +import org.apache.commons.lang3.tuple.Pair; +import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable; + +/** + * Container to bag the S C & O data for an abstract file node. + * + */ +class SCOData { + + private Pair scoreAndDescription = null; + private DataResultViewerTable.HasCommentStatus comment = null; + private Pair countAndDescription = null; + + Pair getScoreAndDescription() { + return scoreAndDescription; + } + + DataResultViewerTable.HasCommentStatus getComment() { + return comment; + } + + Pair getCountAndDescription() { + return countAndDescription; + } + + void setScoreAndDescription(Pair scoreAndDescription) { + this.scoreAndDescription = scoreAndDescription; + } + void setComment(DataResultViewerTable.HasCommentStatus comment) { + this.comment = comment; + } + void setCountAndDescription(Pair countAndDescription) { + this.countAndDescription = countAndDescription; + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/SlackFileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/SlackFileNode.java index 49d1b9da54..0e7bce56cc 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/SlackFileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/SlackFileNode.java @@ -28,6 +28,7 @@ import org.openide.util.Utilities; import org.sleuthkit.autopsy.actions.AddContentTagAction; import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; +import org.sleuthkit.autopsy.directorytree.ExportCSVAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; import org.sleuthkit.autopsy.directorytree.NewWindowViewAction; import org.sleuthkit.autopsy.directorytree.ViewContextAction; @@ -85,6 +86,7 @@ public class SlackFileNode extends AbstractFsContentNode { NbBundle.getMessage(this.getClass(), "SlackFileNode.getActions.viewInNewWin.text"), this)); actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/SpecialDirectoryNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/SpecialDirectoryNode.java index 47f616de12..ce5948ce68 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/SpecialDirectoryNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/SpecialDirectoryNode.java @@ -25,6 +25,7 @@ import javax.swing.Action; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.datasourcesummary.ViewSummaryInformationAction; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; +import org.sleuthkit.autopsy.directorytree.ExportCSVAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; import org.sleuthkit.autopsy.directorytree.FileSearchAction; import org.sleuthkit.autopsy.directorytree.NewWindowViewAction; @@ -61,6 +62,7 @@ public abstract class SpecialDirectoryNode extends AbstractAbstractFileNode { public String getItemType() { return DisplayableItemNode.FILE_PARENT_NODE_KEY; } + /** + * Reads and returns a list of all tags associated with this content node. + * + * Null implementation of an abstract method. + * + * @return list of tags associated with the node. + */ + @Override + protected List getAllTagsFromDatabase() { + return new ArrayList<>(); + } + /** + * Returns correlation attribute instance for the underlying content of the node. + * + * Null implementation of an abstract method. + * + * @return correlation attribute instance for the underlying content of the node. + */ + @Override + protected CorrelationAttributeInstance getCorrelationAttributeInstance() { + return null; + } + + /** + * Returns Score property for the node. + * + * Null implementation of an abstract method. + * + * @param tags list of tags. + * + * @return Score property for the underlying content of the node. + */ + @Override + protected Pair getScorePropertyAndDescription(List tags) { + return Pair.of(DataResultViewerTable.Score.NO_SCORE, NO_DESCR); + } + /** + * Returns comment property for the node. + * + * Null implementation of an abstract method. + * + * @param tags list of tags + * @param attribute correlation attribute instance + * + * @return Comment property for the underlying content of the node. + */ + @Override + protected DataResultViewerTable.HasCommentStatus getCommentProperty(List tags, CorrelationAttributeInstance attribute) { + return DataResultViewerTable.HasCommentStatus.NO_COMMENT; + } + /** + * Returns occurrences/count property for the node. + * + * Null implementation of an abstract method. + * + * @param attribute correlation attribute instance + * + * @return count property for the underlying content of the node. + */ + @Override + protected Pair getCountPropertyAndDescription(CorrelationAttributeInstance attribute) { + return Pair.of(-1L, NO_DESCR); + } } diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties-MERGED index 0d04cc1c46..f70a374bc5 100755 --- a/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties-MERGED @@ -1,3 +1,8 @@ +CSVWriter.done.notifyMsg.error=Error exporting to CSV file +# {0} - Output file +CSVWriter.done.notifyMsg.success=Wrote to {0} +CSVWriter.progress.cancelling=Cancelling +CSVWriter.progress.extracting=Exporting to CSV file CTL_DirectoryTreeTopComponent=Directory Tree DataResultFilterNode.viewSourceArtifact.text=View Source Result # {0} - dataSourceCount @@ -5,6 +10,11 @@ DirectoryTreeTopComponent.componentOpened.groupDataSources.text=This case contai DirectoryTreeTopComponent.componentOpened.groupDataSources.title=Group by data source? DirectoryTreeTopComponent.emptyMimeNode.text=Data not available. Run file type identification module. DirectoryTreeTopComponent.resultsView.title=Listing +ExportCSV.saveNodesToCSV.empty=No data to export +# {0} - Output file +ExportCSV.saveNodesToCSV.fileExists=File {0} already exists +ExportCSV.saveNodesToCSV.noCurrentCase=No open case available +ExportCSV.title.text=Export selected rows to CSV ExternalViewerAction.actionPerformed.failure.exe.message=The file is an executable and will not be opened. ExternalViewerAction.actionPerformed.failure.IO.message=There is no associated editor for files of this type or the associated application failed to launch. ExternalViewerAction.actionPerformed.failure.missingFile.message=The file no longer exists. diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java b/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java index 0b3a6f698f..d438c11727 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java @@ -372,6 +372,7 @@ public class DataResultFilterNode extends FilterNode { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); actionsList.add(AddBlackboardArtifactTagAction.getInstance()); diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ExplorerNodeActionVisitor.java b/Core/src/org/sleuthkit/autopsy/directorytree/ExplorerNodeActionVisitor.java index 99692ea633..674eb6bf2e 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/ExplorerNodeActionVisitor.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ExplorerNodeActionVisitor.java @@ -125,6 +125,7 @@ public class ExplorerNodeActionVisitor extends ContentVisitor.Default visit(final DerivedFile d) { List actionsList = new ArrayList<>(); actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(AddContentTagAction.getInstance()); final Collection selectedFilesList = @@ -166,6 +169,7 @@ public class ExplorerNodeActionVisitor extends ContentVisitor.Default visit(final LocalFile d) { List actionsList = new ArrayList<>(); actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(AddContentTagAction.getInstance()); final Collection selectedFilesList = @@ -182,6 +186,7 @@ public class ExplorerNodeActionVisitor extends ContentVisitor.Default visit(final org.sleuthkit.datamodel.File d) { List actionsList = new ArrayList<>(); actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(AddContentTagAction.getInstance()); final Collection selectedFilesList = diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ExportCSVAction.java b/Core/src/org/sleuthkit/autopsy/directorytree/ExportCSVAction.java new file mode 100644 index 0000000000..7761e82231 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ExportCSVAction.java @@ -0,0 +1,360 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this content 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.directorytree; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.io.File; +import java.io.BufferedWriter; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.lang.reflect.InvocationTargetException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import javax.swing.AbstractAction; +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; +import javax.swing.SwingWorker; +import javax.swing.filechooser.FileNameExtensionFilter; +import org.netbeans.api.progress.ProgressHandle; +import org.openide.util.Cancellable; +import org.openide.util.NbBundle; +import org.openide.util.Utilities; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.datamodel.AbstractAbstractFileNode.AbstractFilePropertyType; +import org.openide.nodes.Node; +import org.openide.nodes.Node.PropertySet; +import org.openide.nodes.Node.Property; + +/** + * Exports CSV version of result nodes to a location selected by the user. + */ +public final class ExportCSVAction extends AbstractAction { + + private static final Logger logger = Logger.getLogger(ExportCSVAction.class.getName()); + private final static String DEFAULT_FILENAME = "Results"; + private final static List columnsToSkip = Arrays.asList(AbstractFilePropertyType.SCORE.toString(), + AbstractFilePropertyType.COMMENT.toString(), AbstractFilePropertyType.OCCURRENCES.toString()); + + private static String userDefinedExportPath; + + // This class is a singleton to support multi-selection of nodes, since + // org.openide.nodes.NodeOp.findActions(Node[] nodes) will only pick up an Action if every + // node in the array returns a reference to the same action object from Node.getActions(boolean). + private static ExportCSVAction instance; + + /** + * Get an instance of the Action. See above for why + * the class is a singleton. + * + * @return the instance + */ + public static synchronized ExportCSVAction getInstance() { + if (null == instance) { + instance = new ExportCSVAction(); + } + return instance; + } + + /** + * Private constructor for the action. + */ + @NbBundle.Messages({"ExportCSV.title.text=Export selected rows to CSV"}) + private ExportCSVAction() { + super(Bundle.ExportCSV_title_text()); + } + + /** + * Asks user to choose destination, then extracts content to destination + * (recursing on directories). + * + * @param e The action event. + */ + + @Override + public void actionPerformed(ActionEvent e) { + Collection selectedNodes = Utilities.actionsGlobalContext().lookupAll(Node.class); + saveNodesToCSV(selectedNodes, (Component)e.getSource()); + } + + /** + * Save the selected nodes to a CSV file + * + * @param nodesToExport the nodes to save + * @param component + */ + @NbBundle.Messages({ + "# {0} - Output file", + "ExportCSV.saveNodesToCSV.fileExists=File {0} already exists", + "ExportCSV.saveNodesToCSV.noCurrentCase=No open case available", + "ExportCSV.saveNodesToCSV.empty=No data to export"}) + public static void saveNodesToCSV(Collection nodesToExport, Component component) { + + if (nodesToExport.isEmpty()) { + MessageNotifyUtil.Message.info(Bundle.ExportCSV_saveNodesToCSV_empty()); + return; + } + + try { + // Set up the file chooser with a default name and either the Export + // folder or the last used folder. + String fileName = getDefaultOutputFileName(nodesToExport.iterator().next().getParentNode()); + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setCurrentDirectory(new File(getExportDirectory(Case.getCurrentCaseThrows()))); + fileChooser.setSelectedFile(new File(fileName)); + fileChooser.setFileFilter(new FileNameExtensionFilter("csv file", "csv")); + + int returnVal = fileChooser.showSaveDialog(component); + if (returnVal == JFileChooser.APPROVE_OPTION) { + + // Get the file name, appending .csv if necessary + File selectedFile = fileChooser.getSelectedFile(); + if (!selectedFile.getName().endsWith(".csv")) { // NON-NLS + selectedFile = new File(selectedFile.toString() + ".csv"); // NON-NLS + } + + // Save the directory used for next time + updateExportDirectory(selectedFile.getParent(), Case.getCurrentCaseThrows()); + + if (selectedFile.exists()) { + logger.log(Level.SEVERE, "File {0} already exists", selectedFile.getAbsolutePath()); //NON-NLS + MessageNotifyUtil.Message.info(Bundle.ExportCSV_saveNodesToCSV_fileExists(selectedFile)); + return; + } + + CSVWriter writer = new CSVWriter(nodesToExport, selectedFile); + writer.execute(); + } + } catch (NoCurrentCaseException ex) { + JOptionPane.showMessageDialog(component, Bundle.ExportCSV_saveNodesToCSV_noCurrentCase()); + logger.log(Level.INFO, "Exception while getting open case.", ex); //NON-NLS + } + } + + /** + * Create a default name for the CSV output. + * + * @param parent The parent node for the selected nodes + * + * @return the default name + */ + private static String getDefaultOutputFileName(Node parent) { + String dateStr = String.format("%1$tY%1$tm%1$te%1$tI%1$tM%1$tS", Calendar.getInstance()); + + if (parent != null) { + // The first value in the property set is generally a reasonable name + for (PropertySet set : parent.getPropertySets()) { + for (Property prop : set.getProperties()) { + try { + String parentName = prop.getValue().toString(); + + // Strip off the count (if present) + parentName = parentName.replaceAll("\\([0-9]+\\)$", ""); + + // Strip out any invalid characters + parentName = parentName.replaceAll("[\\\\/:*?\"<>|]", "_"); + + return parentName + " " + dateStr; + } catch (IllegalAccessException | InvocationTargetException ex) { + logger.log(Level.WARNING, "Failed to get property set value as string", ex); + } + } + } + } + return DEFAULT_FILENAME + " " + dateStr; + } + + /** + * Get the export directory path. + * + * @param openCase The current case. + * + * @return The export directory path. + */ + private static String getExportDirectory(Case openCase) { + String caseExportPath = openCase.getExportDirectory(); + + if (userDefinedExportPath == null) { + return caseExportPath; + } + + File file = new File(userDefinedExportPath); + if (file.exists() == false || file.isDirectory() == false) { + return caseExportPath; + } + + return userDefinedExportPath; + } + + /** + * Update the default export directory. If the directory path matches the + * case export directory, then the directory used will always match the + * export directory of any given case. Otherwise, the path last used will be + * saved. + * + * @param exportPath The export path. + * @param openCase The current case. + */ + private static void updateExportDirectory(String exportPath, Case openCase) { + if (exportPath.equalsIgnoreCase(openCase.getExportDirectory())) { + userDefinedExportPath = null; + } else { + userDefinedExportPath = exportPath; + } + } + + + /** + * Thread that does the actual extraction work + */ + private static class CSVWriter extends SwingWorker { + + private static final Logger logger = Logger.getLogger(CSVWriter.class.getName()); + private ProgressHandle progress; + + private final Collection nodesToExport; + private final File outputFile; + + /** + * Create an instance of the CSVWriter. + * + * @param extractionTasks List of file extraction tasks. + */ + CSVWriter(Collection nodesToExport, File outputFile) { + this.nodesToExport = nodesToExport; + this.outputFile = outputFile; + } + + @NbBundle.Messages({"CSVWriter.progress.extracting=Exporting to CSV file", + "CSVWriter.progress.cancelling=Cancelling"}) + @Override + protected Object doInBackground() throws Exception { + if (nodesToExport.isEmpty()) { + return null; + } + + // Set up progress bar. + final String displayName = Bundle.CSVWriter_progress_extracting(); + progress = ProgressHandle.createHandle(displayName, new Cancellable() { + @Override + public boolean cancel() { + if (progress != null) { + progress.setDisplayName(Bundle.CSVWriter_progress_cancelling()); + } + return ExportCSVAction.CSVWriter.this.cancel(true); + } + }); + progress.start(); + progress.switchToIndeterminate(); + + try (BufferedWriter br = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outputFile), StandardCharsets.UTF_8))) { + // Write BOM + br.write('\ufeff'); + + // Write the header + List headers = new ArrayList<>(); + PropertySet[] sets = nodesToExport.iterator().next().getPropertySets(); + for(PropertySet set : sets) { + for (Property prop : set.getProperties()) { + if ( ! columnsToSkip.contains(prop.getDisplayName())) { + headers.add(prop.getDisplayName()); + } + } + } + br.write(listToCSV(headers)); + + // Write each line + Iterator nodeIterator = nodesToExport.iterator(); + while (nodeIterator.hasNext()) { + if (this.isCancelled()) { + break; + } + + Node node = (Node)nodeIterator.next(); + List values = new ArrayList<>(); + sets = node.getPropertySets(); + for(PropertySet set : sets) { + for (Property prop : set.getProperties()) { + if ( ! columnsToSkip.contains(prop.getDisplayName())) { + values.add(escapeQuotes(prop.getValue().toString())); + } + } + } + br.write(listToCSV(values)); + } + } + + return null; + } + + /** + * Escape any quotes in the string + * + * @param original + * + * @return the string with quotes escaped + */ + private String escapeQuotes(String original) { + return original.replaceAll("\"", "\\\\\""); + } + + /** + * Convert list of values to a comma separated string. + * + * @param values Values to convert + * + * @return values as CSV + */ + private String listToCSV(List values) { + return "\"" + String.join("\",\"", values) + "\"\n"; + } + + @NbBundle.Messages({"CSVWriter.done.notifyMsg.error=Error exporting to CSV file", + "# {0} - Output file", + "CSVWriter.done.notifyMsg.success=Wrote to {0}"}) + @Override + protected void done() { + boolean msgDisplayed = false; + try { + super.get(); + } catch (InterruptedException | ExecutionException ex) { + logger.log(Level.SEVERE, "Fatal error during file extraction", ex); //NON-NLS + MessageNotifyUtil.Message.info(Bundle.CSVWriter_done_notifyMsg_error()); + msgDisplayed = true; + } catch (java.util.concurrent.CancellationException ex) { + // catch and ignore if we were cancelled + } finally { + progress.finish(); + if (!this.isCancelled() && !msgDisplayed) { + MessageNotifyUtil.Message.info(Bundle.CSVWriter_done_notifyMsg_success(outputFile)); + } + } + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java index e919bf189f..d226e6765b 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java @@ -278,6 +278,7 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp //enable the new button FilesSetDefsPanel.this.newSetButton.setEnabled(canBeEnabled); FilesSetDefsPanel.this.importSetButton.setEnabled(canBeEnabled); + // Get the selected interesting files set and populate the set // components. FilesSet selectedSet = FilesSetDefsPanel.this.setsList.getSelectedValue(); @@ -302,6 +303,12 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp if (!FilesSetDefsPanel.this.rulesListModel.isEmpty()) { FilesSetDefsPanel.this.rulesList.setSelectedIndex(0); } + } else { + // Disable the edit, delete, copy, and export buttons + FilesSetDefsPanel.this.editSetButton.setEnabled(false); + FilesSetDefsPanel.this.deleteSetButton.setEnabled(false); + FilesSetDefsPanel.this.copySetButton.setEnabled(false); + FilesSetDefsPanel.this.exportSetButton.setEnabled(false); } } diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/TextTranslationService.java b/Core/src/org/sleuthkit/autopsy/texttranslation/TextTranslationService.java index f1e6f084b7..b24dca2aac 100755 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/TextTranslationService.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/TextTranslationService.java @@ -120,4 +120,13 @@ public final class TextTranslationService { public boolean hasProvider() { return selectedTranslator.isPresent(); } + + /** + * Returns the hard limit for translation request sizes. + * + * @return + */ + public int getMaxPayloadSize() { + return selectedTranslator.get().getMaxPayloadSize(); + } } diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/TextTranslator.java b/Core/src/org/sleuthkit/autopsy/texttranslation/TextTranslator.java index 5e6ad8b19e..e996e6331e 100755 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/TextTranslator.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/TextTranslator.java @@ -56,4 +56,11 @@ public interface TextTranslator { * Save the settings as they have been modified in the component. */ void saveSettings(); + + /** + * Returns the hard limit for translation request sizes. + * + * @return + */ + int getMaxPayloadSize(); } diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/translators/BingTranslator.java b/Core/src/org/sleuthkit/autopsy/texttranslation/translators/BingTranslator.java index 2b234f71d3..3dfd984b0a 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/translators/BingTranslator.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/translators/BingTranslator.java @@ -108,7 +108,6 @@ public class BingTranslator implements TextTranslator { String toTranslate = string.trim(); //Translates some text into English, without specifying the source langauge. - // HTML files were producing lots of white space at the end //Google Translate required us to replace (\r\n|\n) with
//but Bing Translator doesn not have that requirement. //The free account has a maximum file size. If you have a paid account, @@ -172,4 +171,9 @@ public class BingTranslator implements TextTranslator { throw new TranslationException("JSON text does not match Bing Translator scheme: " + e); } } + + @Override + public int getMaxPayloadSize() { + return MAX_STRING_LENGTH; + } } diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/translators/GoogleTranslator.java b/Core/src/org/sleuthkit/autopsy/texttranslation/translators/GoogleTranslator.java index 46bdd6da67..8289010828 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/translators/GoogleTranslator.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/translators/GoogleTranslator.java @@ -47,7 +47,8 @@ import org.sleuthkit.autopsy.texttranslation.TranslationException; public final class GoogleTranslator implements TextTranslator { private static final Logger logger = Logger.getLogger(GoogleTranslator.class.getName()); - private static final int MAX_STRING_LENGTH = 15000; + //See translate method for justification of this limit. + private static final int MAX_PAYLOAD_SIZE = 5000; private final GoogleTranslatorSettingsPanel settingsPanel; private final GoogleTranslatorSettings settings = new GoogleTranslatorSettings(); private Translate googleTranslate; @@ -90,21 +91,20 @@ public final class GoogleTranslator implements TextTranslator { if (googleTranslate != null) { try { // Translates some text into English, without specifying the source language. - - // HTML files were producing lots of white space at the end String substring = string.trim(); // We can't currently set parameters, so we are using the default behavior of // assuming the input is HTML. We need to replace newlines with
for Google to preserve them substring = substring.replaceAll("(\r\n|\n)", "
"); - // The API complains if the "Payload" is over 204800 bytes. I'm assuming that - // deals with the full request. At some point, we get different errors about too - // much text. Officially, Google says they will googleTranslate only 5k chars, - // but we have seen more than that working. - // there could be a value betwen 15k and 25k that works. I (BC) didn't test further - if (substring.length() > MAX_STRING_LENGTH) { - substring = substring.substring(0, MAX_STRING_LENGTH); + // The API complains if the "Payload" is over 204800 bytes. Google references that + //their service is optimized for 2K code points and recommends keeping the requests that size. + //There is a hard limit of 30K code points per request. There is also a time-based quota that + //we are not enforcing, which may lead to 403 errors. We are currently configured for a max of 5K + //in each request, for two reasons. 1) To be more in line with Google's recommendation. 2) To + //minimize accidental exceedence of time based quotas. + if (substring.length() > MAX_PAYLOAD_SIZE) { + substring = substring.substring(0, MAX_PAYLOAD_SIZE); } Translation translation = googleTranslate.translate(substring); @@ -178,4 +178,9 @@ public final class GoogleTranslator implements TextTranslator { settings.saveSettings(); loadTranslator(); } + + @Override + public int getMaxPayloadSize() { + return MAX_PAYLOAD_SIZE; + } } diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties index 9f01ce498c..a62bfce462 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties @@ -1,7 +1,6 @@ OptionsCategory_Name_Machine_Translation=Machine Translation OptionsCategory_Keywords_Machine_Translation_Settings=Machine Translation Settings TranslationContentPanel.ShowLabel.text=Show: -TranslationContentPanel.warningLabel2MB.text=Only the first 1MB of text will be displayed TranslationContentPanel.ocrLabel.text=OCR: TranslationOptionsPanel.translationServiceLabel.text=Text translator: TranslationOptionsPanelController.moduleErr=Module Error diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties-MERGED index 00718e5aa1..08a9b5d60c 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties-MERGED @@ -1,6 +1,6 @@ OptionsCategory_Name_Machine_Translation=Machine Translation OptionsCategory_Keywords_Machine_Translation_Settings=Machine Translation Settings -TranslatedContentPanel.comboBoxOption.originalText=Original Text +TranslatedContentPanel.comboBoxOption.originalText=Original Text (Up to 25KB) TranslatedContentPanel.comboBoxOption.translatedText=Translated Text TranslatedContentViewer.emptyTranslation=The resulting translation was empty. TranslatedContentViewer.errorExtractingText=Could not extract text from file. @@ -12,11 +12,11 @@ TranslatedContentViewer.noServiceProvider=Machine Translation software was not f TranslatedContentViewer.textAlreadyIndexed=Please view the original text in the Indexed Text viewer. TranslatedContentViewer.translatingText=Translating text, please wait... TranslatedContentViewer.translationException=Error encountered while attempting translation. +TranslatedTextViewer.maxPayloadSize=Up to the first %dKB of text will be translated TranslatedTextViewer.title=Translation TranslatedTextViewer.toolTip=Displays translated file text. TranslationContentPanel.autoDetectOCR=Autodetect language TranslationContentPanel.ShowLabel.text=Show: -TranslationContentPanel.warningLabel2MB.text=Only the first 1MB of text will be displayed TranslationContentPanel.ocrLabel.text=OCR: TranslationOptionsPanel.noTextTranslators.text=No text translators exist, translation is disabled. TranslationOptionsPanel.noTextTranslatorSelected.text=No text translator selected, translation is disabled. diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java index e9656bb0a9..b111f431d9 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java @@ -67,7 +67,7 @@ public final class TranslatedTextViewer implements TextViewer { private static final boolean OCR_ENABLED = true; private static final boolean OCR_DISABLED = false; - private static final int MAX_SIZE_1MB = 1024000; + private static final int MAX_EXTRACT_SIZE_BYTES = 25600; private static final List INSTALLED_LANGUAGE_PACKS = PlatformUtil.getOcrLanguagePacks(); private final TranslationContentPanel panel = new TranslationContentPanel(); @@ -77,6 +77,9 @@ public final class TranslatedTextViewer implements TextViewer { = new ThreadFactoryBuilder().setNameFormat("translation-content-viewer-%d").build(); private final ExecutorService executorService = Executors.newSingleThreadExecutor(translationThreadFactory); + @NbBundle.Messages({ + "TranslatedTextViewer.maxPayloadSize=Up to the first %dKB of text will be translated" + }) @Override public void setNode(final Node node) { this.node = node; @@ -92,6 +95,9 @@ public final class TranslatedTextViewer implements TextViewer { panel.addLanguagePackNames(INSTALLED_LANGUAGE_PACKS); } } + + int payloadMaxInKB = TextTranslationService.getInstance().getMaxPayloadSize() / 1000; + panel.setWarningLabelMsg(String.format(Bundle.TranslatedTextViewer_maxPayloadSize(), payloadMaxInKB)); //Force a background task. displayDropDownListener.actionPerformed(null); @@ -296,8 +302,9 @@ public final class TranslatedTextViewer implements TextViewer { //Correct for UTF-8 byte[] resultInUTF8Bytes = result.getBytes("UTF8"); - byte[] trimTo1MB = Arrays.copyOfRange(resultInUTF8Bytes, 0, MAX_SIZE_1MB ); - return new String(trimTo1MB, "UTF-8"); + byte[] trimToArraySize = Arrays.copyOfRange(resultInUTF8Bytes, 0, + Math.min(resultInUTF8Bytes.length, MAX_EXTRACT_SIZE_BYTES) ); + return new String(trimToArraySize, "UTF-8"); } /** @@ -331,7 +338,7 @@ public final class TranslatedTextViewer implements TextViewer { //Short-circuit the read if its greater than our max //translatable size - int bytesLeft = MAX_SIZE_1MB - bytesRead; + int bytesLeft = MAX_EXTRACT_SIZE_BYTES - bytesRead; if (bytesLeft < read) { textBuilder.append(cbuf, 0, bytesLeft); @@ -341,9 +348,8 @@ public final class TranslatedTextViewer implements TextViewer { textBuilder.append(cbuf, 0, read); bytesRead += read; } - - // The trim is on here because HTML files were observed with nearly 1MB of white space at the end - return textBuilder.toString().trim(); + + return textBuilder.toString(); } /** diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslationContentPanel.form b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslationContentPanel.form index e042876476..c64c71bb8a 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslationContentPanel.form +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslationContentPanel.form @@ -16,7 +16,7 @@ - + @@ -48,12 +48,12 @@ - - + + - - - + + + @@ -72,18 +72,25 @@
- - - - - - - - + + + + + + + + + + + + + + + + + - - @@ -153,14 +160,11 @@ - + - - - diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslationContentPanel.java b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslationContentPanel.java index 0e625f1baa..9091cad622 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslationContentPanel.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslationContentPanel.java @@ -87,6 +87,10 @@ public class TranslationContentPanel extends javax.swing.JPanel { dropDown.removeActionListener(listener); } } + + void setWarningLabelMsg(String msg) { + warningLabel.setText(msg); + } @NbBundle.Messages({"TranslationContentPanel.autoDetectOCR=Autodetect language"}) final void reset() { @@ -246,7 +250,7 @@ public class TranslationContentPanel extends javax.swing.JPanel { /** * Selection choices to be displayed in the combobox dropdown. */ - @Messages({"TranslatedContentPanel.comboBoxOption.originalText=Original Text", + @Messages({"TranslatedContentPanel.comboBoxOption.originalText=Original Text (Up to 25KB)", "TranslatedContentPanel.comboBoxOption.translatedText=Translated Text"}) static enum DisplayDropdownOptions { ORIGINAL_TEXT(Bundle.TranslatedContentPanel_comboBoxOption_originalText()), @@ -280,7 +284,7 @@ public class TranslationContentPanel extends javax.swing.JPanel { displayTextComboBox = new javax.swing.JComboBox<>(); ocrDropdown = new javax.swing.JComboBox<>(); ocrLabel = new javax.swing.JLabel(); - warningLabel2MB = new javax.swing.JLabel(); + warningLabel = new javax.swing.JLabel(); jScrollPane1 = new javax.swing.JScrollPane(); displayTextArea = new javax.swing.JTextArea(); @@ -302,8 +306,7 @@ public class TranslationContentPanel extends javax.swing.JPanel { org.openide.awt.Mnemonics.setLocalizedText(ocrLabel, org.openide.util.NbBundle.getMessage(TranslationContentPanel.class, "TranslationContentPanel.ocrLabel.text")); // NOI18N ocrLabel.setEnabled(false); - warningLabel2MB.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/warning16.png"))); // NOI18N - org.openide.awt.Mnemonics.setLocalizedText(warningLabel2MB, org.openide.util.NbBundle.getMessage(TranslationContentPanel.class, "TranslationContentPanel.warningLabel2MB.text")); // NOI18N + warningLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/warning16.png"))); // NOI18N javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1); jPanel1.setLayout(jPanel1Layout); @@ -311,12 +314,12 @@ public class TranslationContentPanel extends javax.swing.JPanel { jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel1Layout.createSequentialGroup() .addContainerGap() - .addComponent(warningLabel2MB) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(warningLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 422, Short.MAX_VALUE) .addComponent(ShowLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(displayTextComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 128, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(displayTextComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 150, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) .addComponent(ocrLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 26, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(ocrDropdown, javax.swing.GroupLayout.PREFERRED_SIZE, 180, javax.swing.GroupLayout.PREFERRED_SIZE) @@ -331,16 +334,20 @@ public class TranslationContentPanel extends javax.swing.JPanel { ); jPanel1Layout.setVerticalGroup( jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel1Layout.createSequentialGroup() + .addGroup(jPanel1Layout.createSequentialGroup() .addGap(9, 9, 9) - .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) - .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(displayTextComboBox, javax.swing.GroupLayout.DEFAULT_SIZE, 26, Short.MAX_VALUE) - .addComponent(ShowLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(ocrLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(warningLabel2MB)) - .addComponent(ocrDropdown)) - .addGap(7, 7, 7)) + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel1Layout.createSequentialGroup() + .addComponent(warningLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addContainerGap()) + .addGroup(jPanel1Layout.createSequentialGroup() + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(displayTextComboBox, javax.swing.GroupLayout.DEFAULT_SIZE, 26, Short.MAX_VALUE) + .addComponent(ShowLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(ocrLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addComponent(ocrDropdown)) + .addGap(7, 7, 7)))) .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(jPanel1Layout.createSequentialGroup() .addContainerGap() @@ -362,7 +369,7 @@ public class TranslationContentPanel extends javax.swing.JPanel { this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(jPanel1, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 628, Short.MAX_VALUE) + .addComponent(jPanel1, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 872, Short.MAX_VALUE) .addComponent(jScrollPane1) ); layout.setVerticalGroup( @@ -384,6 +391,6 @@ public class TranslationContentPanel extends javax.swing.JPanel { private javax.swing.JSeparator jSeparator2; private javax.swing.JComboBox ocrDropdown; private javax.swing.JLabel ocrLabel; - private javax.swing.JLabel warningLabel2MB; + private javax.swing.JLabel warningLabel; // End of variables declaration//GEN-END:variables } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AdHocSearchFilterNode.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AdHocSearchFilterNode.java index 57b84b11d6..cff13cc16e 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AdHocSearchFilterNode.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AdHocSearchFilterNode.java @@ -32,6 +32,7 @@ import org.openide.util.NbBundle; import org.openide.util.Utilities; import org.openide.util.lookup.ProxyLookup; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; +import org.sleuthkit.autopsy.directorytree.ExportCSVAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; import org.sleuthkit.autopsy.actions.AddContentTagAction; @@ -163,6 +164,7 @@ class AdHocSearchFilterNode extends FilterNode { actionsList.add(new ExternalViewerAction(NbBundle.getMessage(this.getClass(), "KeywordSearchFilterNode.getFileActions.openExternViewActLbl"), getOriginal())); actionsList.add(null); actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); diff --git a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/VcardParser.java b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/VcardParser.java index 15d52b536e..7e5a321313 100755 --- a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/VcardParser.java +++ b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/VcardParser.java @@ -143,8 +143,9 @@ final class VcardParser { * @throws NoCurrentCaseException If there is no open case. */ void parse(File vcardFile, AbstractFile abstractFile) throws IOException, NoCurrentCaseException { - VCard vcard = Ezvcard.parse(vcardFile).first(); - addContactArtifact(vcard, abstractFile); + for (VCard vcard: Ezvcard.parse(vcardFile).all()) { + addContactArtifact(vcard, abstractFile); + } }