/* * Autopsy Forensic Browser * * Copyright 2011-2013 Basis Technology Corp. * Contact: carrier <at> sleuthkit <dot> org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.sleuthkit.autopsy.corecomponents; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.logging.Level; import javax.swing.JMenuItem; import javax.swing.JTextPane; import javax.swing.SwingWorker; import org.openide.nodes.Node; import org.openide.util.Lookup; import org.openide.util.NbBundle; import org.openide.util.lookup.ServiceProvider; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.contentviewers.Utilities; import org.sleuthkit.autopsy.corecomponentinterfaces.DataContentViewer; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.datamodel.ArtifactStringContent; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskException; /** * Instances of this class display the BlackboardArtifacts associated with the * Content represented by a Node. Each BlackboardArtifact is rendered as an HTML * representation of its BlackboardAttributes. */ @ServiceProvider(service = DataContentViewer.class, position = 3) public class DataContentViewerArtifact extends javax.swing.JPanel implements DataContentViewer { private final static Logger logger = Logger.getLogger(DataContentViewerArtifact.class.getName()); private final static String WAIT_TEXT = NbBundle.getMessage(DataContentViewerArtifact.class, "DataContentViewerArtifact.waitText"); private final static String ERROR_TEXT = NbBundle.getMessage(DataContentViewerArtifact.class, "DataContentViewerArtifact.errorText"); private Node currentNode; // @@@ Remove this when the redundant setNode() calls problem is fixed. private int currentPage = 1; private final Object lock = new Object(); private List<ArtifactStringContent> artifactContentStrings; // Accessed by multiple threads, use getArtifactContentStrings() and setArtifactContentStrings() SwingWorker<ViewUpdate, Void> currentTask; // Accessed by multiple threads, use startNewTask() public DataContentViewerArtifact() { initComponents(); customizeComponents(); resetComponents(); } /** * This method is called from within the constructor to initialize the form. * WARNING: Do NOT modify this code. The content of this method is always * regenerated by the Form Editor. */ @SuppressWarnings("unchecked") // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents private void initComponents() { rightClickMenu = new javax.swing.JPopupMenu(); copyMenuItem = new javax.swing.JMenuItem(); selectAllMenuItem = new javax.swing.JMenuItem(); jPanel1 = new javax.swing.JPanel(); jScrollPane1 = new javax.swing.JScrollPane(); outputViewPane = new JTextPane(){ public boolean getScrollableTracksViewportWidth() { return (getSize().width < 400); }}; totalPageLabel = new javax.swing.JLabel(); ofLabel = new javax.swing.JLabel(); currentPageLabel = new javax.swing.JLabel(); pageLabel = new javax.swing.JLabel(); nextPageButton = new javax.swing.JButton(); pageLabel2 = new javax.swing.JLabel(); prevPageButton = new javax.swing.JButton(); copyMenuItem.setText(org.openide.util.NbBundle.getMessage(DataContentViewerArtifact.class, "DataContentViewerArtifact.copyMenuItem.text")); // NOI18N rightClickMenu.add(copyMenuItem); selectAllMenuItem.setText(org.openide.util.NbBundle.getMessage(DataContentViewerArtifact.class, "DataContentViewerArtifact.selectAllMenuItem.text")); // NOI18N rightClickMenu.add(selectAllMenuItem); setPreferredSize(new java.awt.Dimension(622, 424)); jPanel1.setPreferredSize(new java.awt.Dimension(622, 424)); outputViewPane.setEditable(false); outputViewPane.setPreferredSize(new java.awt.Dimension(700, 400)); jScrollPane1.setViewportView(outputViewPane); totalPageLabel.setText(org.openide.util.NbBundle.getMessage(DataContentViewerArtifact.class, "DataContentViewerArtifact.totalPageLabel.text")); // NOI18N ofLabel.setText(org.openide.util.NbBundle.getMessage(DataContentViewerArtifact.class, "DataContentViewerArtifact.ofLabel.text")); // NOI18N currentPageLabel.setText(org.openide.util.NbBundle.getMessage(DataContentViewerArtifact.class, "DataContentViewerArtifact.currentPageLabel.text")); // NOI18N currentPageLabel.setMaximumSize(new java.awt.Dimension(18, 14)); currentPageLabel.setMinimumSize(new java.awt.Dimension(18, 14)); currentPageLabel.setPreferredSize(new java.awt.Dimension(18, 14)); pageLabel.setText(org.openide.util.NbBundle.getMessage(DataContentViewerArtifact.class, "DataContentViewerArtifact.pageLabel.text")); // NOI18N pageLabel.setMaximumSize(new java.awt.Dimension(33, 14)); pageLabel.setMinimumSize(new java.awt.Dimension(33, 14)); pageLabel.setPreferredSize(new java.awt.Dimension(33, 14)); nextPageButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/corecomponents/btn_step_forward.png"))); // NOI18N NON-NLS nextPageButton.setText(org.openide.util.NbBundle.getMessage(DataContentViewerArtifact.class, "DataContentViewerArtifact.nextPageButton.text")); // NOI18N nextPageButton.setBorderPainted(false); nextPageButton.setContentAreaFilled(false); nextPageButton.setDisabledIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/corecomponents/btn_step_forward_disabled.png"))); // NOI18N NON-NLS nextPageButton.setMargin(new java.awt.Insets(2, 0, 2, 0)); nextPageButton.setPreferredSize(new java.awt.Dimension(23, 23)); nextPageButton.setRolloverIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/corecomponents/btn_step_forward_hover.png"))); // NOI18N NON-NLS nextPageButton.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { nextPageButtonActionPerformed(evt); } }); pageLabel2.setText(org.openide.util.NbBundle.getMessage(DataContentViewerArtifact.class, "DataContentViewerArtifact.pageLabel2.text")); // NOI18N pageLabel2.setMaximumSize(new java.awt.Dimension(29, 14)); pageLabel2.setMinimumSize(new java.awt.Dimension(29, 14)); pageLabel2.setPreferredSize(new java.awt.Dimension(29, 14)); prevPageButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/corecomponents/btn_step_back.png"))); // NOI18N NON-NLS prevPageButton.setText(org.openide.util.NbBundle.getMessage(DataContentViewerArtifact.class, "DataContentViewerArtifact.prevPageButton.text")); // NOI18N prevPageButton.setBorderPainted(false); prevPageButton.setContentAreaFilled(false); prevPageButton.setDisabledIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/corecomponents/btn_step_back_disabled.png"))); // NOI18N NON-NLS prevPageButton.setMargin(new java.awt.Insets(2, 0, 2, 0)); prevPageButton.setPreferredSize(new java.awt.Dimension(23, 23)); prevPageButton.setRolloverIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/corecomponents/btn_step_back_hover.png"))); // NOI18N NON-NLS prevPageButton.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { prevPageButtonActionPerformed(evt); } }); javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1); jPanel1.setLayout(jPanel1Layout); jPanel1Layout.setHorizontalGroup( jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(jPanel1Layout.createSequentialGroup() .addContainerGap() .addComponent(pageLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 45, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(currentPageLabel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) .addComponent(ofLabel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) .addComponent(totalPageLabel) .addGap(41, 41, 41) .addComponent(pageLabel2, javax.swing.GroupLayout.PREFERRED_SIZE, 38, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(prevPageButton, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) .addGap(0, 0, 0) .addComponent(nextPageButton, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) .addContainerGap(366, Short.MAX_VALUE)) .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 622, Short.MAX_VALUE) ); jPanel1Layout.setVerticalGroup( jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(jPanel1Layout.createSequentialGroup() .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(pageLabel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(currentPageLabel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(ofLabel) .addComponent(totalPageLabel)) .addComponent(nextPageButton, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(prevPageButton, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(pageLabel2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addGap(0, 0, 0) .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 401, Short.MAX_VALUE)) ); javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(jPanel1, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(jPanel1, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) ); }// </editor-fold>//GEN-END:initComponents private void nextPageButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_nextPageButtonActionPerformed currentPage = currentPage + 1; currentPageLabel.setText(Integer.toString(currentPage)); startNewTask(new SelectedArtifactChangedTask(currentPage)); }//GEN-LAST:event_nextPageButtonActionPerformed private void prevPageButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_prevPageButtonActionPerformed currentPage = currentPage - 1; currentPageLabel.setText(Integer.toString(currentPage)); startNewTask(new SelectedArtifactChangedTask(currentPage)); }//GEN-LAST:event_prevPageButtonActionPerformed // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JMenuItem copyMenuItem; private javax.swing.JLabel currentPageLabel; private javax.swing.JPanel jPanel1; private javax.swing.JScrollPane jScrollPane1; private javax.swing.JButton nextPageButton; private javax.swing.JLabel ofLabel; private javax.swing.JTextPane outputViewPane; private javax.swing.JLabel pageLabel; private javax.swing.JLabel pageLabel2; private javax.swing.JButton prevPageButton; private javax.swing.JPopupMenu rightClickMenu; private javax.swing.JMenuItem selectAllMenuItem; private javax.swing.JLabel totalPageLabel; // End of variables declaration//GEN-END:variables private void customizeComponents() { outputViewPane.setComponentPopupMenu(rightClickMenu); ActionListener actList = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { JMenuItem jmi = (JMenuItem) e.getSource(); if (jmi.equals(copyMenuItem)) { outputViewPane.copy(); } else if (jmi.equals(selectAllMenuItem)) { outputViewPane.selectAll(); } } }; copyMenuItem.addActionListener(actList); selectAllMenuItem.addActionListener(actList); Utilities.configureTextPaneAsHtml(outputViewPane); } /** * Resets the components to an empty view state. */ private void resetComponents() { currentPage = 1; currentPageLabel.setText(""); totalPageLabel.setText(""); outputViewPane.setText(""); prevPageButton.setEnabled(false); nextPageButton.setEnabled(false); currentNode = null; } @Override public void setNode(Node selectedNode) { if (currentNode == selectedNode) { return; } currentNode = selectedNode; // Make sure there is a node. Null might be passed to reset the viewer. if (selectedNode == null) { return; } // Make sure the node is of the correct type. Lookup lookup = selectedNode.getLookup(); Content content = lookup.lookup(Content.class); if (content == null) { return; } startNewTask(new SelectedNodeChangedTask(selectedNode)); } @Override public String getTitle() { return NbBundle.getMessage(this.getClass(), "DataContentViewerArtifact.title"); } @Override public String getToolTip() { return NbBundle.getMessage(this.getClass(), "DataContentViewerArtifact.toolTip"); } @Override public DataContentViewer createInstance() { return new DataContentViewerArtifact(); } @Override public Component getComponent() { return this; } @Override public void resetComponent() { resetComponents(); } @Override public boolean isSupported(Node node) { if (node == null) { return false; } Content content = node.getLookup().lookup(Content.class); if (content != null) { try { return content.getAllArtifactsCount() > 0; } catch (TskException ex) { logger.log(Level.WARNING, "Couldn't get count of BlackboardArtifacts for content", ex); //NON-NLS } } return false; } @Override public int isPreferred(Node node) { BlackboardArtifact artifact = node.getLookup().lookup(BlackboardArtifact.class); // low priority if node doesn't have an artifact (meaning it was found from normal directory // browsing, or if the artifact is something that means the user really wants to see the original // file and not more details about the artifact if ((artifact == null) || (artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID()) || (artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID())) { return 3; } else { return 5; } } /** * Instances of this class are simple containers for view update information * generated by a background thread. */ private class ViewUpdate { int numberOfPages; int currentPage; String text; ViewUpdate(int numberOfPages, int currentPage, String text) { this.currentPage = currentPage; this.numberOfPages = numberOfPages; this.text = text; } } /** * Called from queued SwingWorker done() methods on the EDT thread, so * doesn't need to be synchronized. * * @param viewUpdate A simple container for display update information from * a background thread. */ private void updateView(ViewUpdate viewUpdate) { this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); nextPageButton.setEnabled(viewUpdate.currentPage < viewUpdate.numberOfPages); prevPageButton.setEnabled(viewUpdate.currentPage > 1); currentPage = viewUpdate.currentPage; totalPageLabel.setText(Integer.toString(viewUpdate.numberOfPages)); currentPageLabel.setText(Integer.toString(currentPage)); // @@@ This can take a long time. Perhaps a faster HTML renderer can be found. // Note that the rendering appears to be done on a background thread, since the // wait cursor reset below happens before the new text hits the JTextPane. On the // other hand, the UI is unresponsive... outputViewPane.setText(viewUpdate.text); outputViewPane.moveCaretPosition(0); this.setCursor(null); } /** * Start a new task on its own background thread, canceling the previous * task. * * @param task A new SwingWorker object to execute as a background thread. */ private synchronized void startNewTask(SwingWorker<ViewUpdate, Void> task) { outputViewPane.setText(WAIT_TEXT); outputViewPane.moveCaretPosition(0); // The output of the previous task is no longer relevant. if (currentTask != null) { // This call sets a cancellation flag. It does not terminate the background thread running the task. // The task must check the cancellation flag and react appropriately. currentTask.cancel(false); } // Start the new task. currentTask = task; currentTask.execute(); } /** * Populate the cache of artifact represented as strings. * * @param artifactStrings A list of string representations of artifacts. */ private void setArtifactContentStrings(List<ArtifactStringContent> artifactStrings) { synchronized (lock) { this.artifactContentStrings = artifactStrings; } } /** * Retrieve the cache of artifact represented as strings. * * @return A list of string representations of artifacts. */ private List<ArtifactStringContent> getArtifactContentStrings() { synchronized (lock) { return artifactContentStrings; } } /** * Instances of this class use a background thread to generate a ViewUpdate * when a node is selected, changing the set of blackboard artifacts * ("results") to be displayed. */ private class SelectedNodeChangedTask extends SwingWorker<ViewUpdate, Void> { private final Node selectedNode; SelectedNodeChangedTask(Node selectedNode) { this.selectedNode = selectedNode; } @Override protected ViewUpdate doInBackground() { // Get the lookup for the node for access to its underlying content and // blackboard artifact, if any. Lookup lookup = selectedNode.getLookup(); // Get the content. Content content = lookup.lookup(Content.class); if (content == null) { return new ViewUpdate(getArtifactContentStrings().size(), currentPage, ERROR_TEXT); } // Get all of the blackboard artifacts associated with the content. These are what this // viewer displays. ArrayList<BlackboardArtifact> artifacts; try { artifacts = content.getAllArtifacts(); } catch (TskException ex) { logger.log(Level.WARNING, "Couldn't get artifacts", ex); //NON-NLS return new ViewUpdate(getArtifactContentStrings().size(), currentPage, ERROR_TEXT); } if (isCancelled()) { return null; } // Build the new artifact strings cache. ArrayList<ArtifactStringContent> artifactStrings = new ArrayList<>(); for (BlackboardArtifact artifact : artifacts) { artifactStrings.add(new ArtifactStringContent(artifact)); } // If the node has an underlying blackboard artifact, show it. If not, // show the first artifact. int index = 0; BlackboardArtifact artifact = lookup.lookup(BlackboardArtifact.class); if (artifact != null) { index = artifacts.indexOf(artifact); if (index == -1) { index = 0; } else { // if the artifact has an ASSOCIATED ARTIFACT, then we display the associated artifact instead try { for (BlackboardAttribute attr : artifact.getAttributes()) { if (attr.getAttributeType().getTypeID() == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT.getTypeID()) { long assocArtifactId = attr.getValueLong(); int assocArtifactIndex = -1; for (BlackboardArtifact art : artifacts) { if (assocArtifactId == art.getArtifactID()) { assocArtifactIndex = artifacts.indexOf(art); break; } } if (assocArtifactIndex >= 0) { index = assocArtifactIndex; } break; } } } catch (TskCoreException ex) { logger.log(Level.WARNING, "Couldn't get associated artifact to display in Content Viewer.", ex); //NON-NLS } } } if (isCancelled()) { return null; } // Add one to the index of the artifact string for the corresponding page index. Note that the getString() method // of ArtifactStringContent does a lazy fetch of the attributes of the correspoding artifact and represents them as // HTML. ViewUpdate viewUpdate = new ViewUpdate(artifactStrings.size(), index + 1, artifactStrings.get(index).getString()); // It may take a considerable amount of time to fetch the attributes of the selected artifact and render them // as HTML, so check for cancellation. if (isCancelled()) { return null; } // Update the artifact strings cache. setArtifactContentStrings(artifactStrings); return viewUpdate; } @Override protected void done() { if (!isCancelled()) { try { ViewUpdate viewUpdate = get(); if (viewUpdate != null) { updateView(viewUpdate); } } catch (InterruptedException | ExecutionException ex) { logger.log(Level.WARNING, "Artifact display task unexpectedly interrupted or failed", ex); //NON-NLS } } } } /** * Instances of this class use a background thread to generate a ViewUpdate * when the user pages the view to look at another blackboard artifact * ("result"). */ private class SelectedArtifactChangedTask extends SwingWorker<ViewUpdate, Void> { private final int pageIndex; SelectedArtifactChangedTask(final int pageIndex) { this.pageIndex = pageIndex; } @Override protected ViewUpdate doInBackground() { // Get the artifact string to display from the cache. Note that one must be subtracted from the // page index to get the corresponding artifact string index. List<ArtifactStringContent> artifactStrings = getArtifactContentStrings(); ArtifactStringContent artifactStringContent = artifactStrings.get(pageIndex - 1); // The getString() method of ArtifactStringContent does a lazy fetch of the attributes of the // correspoding artifact and represents them as HTML. String artifactString = artifactStringContent.getString(); // It may take a considerable amount of time to fetch the attributes of the selected artifact and render them // as HTML, so check for cancellation. if (isCancelled()) { return null; } return new ViewUpdate(artifactStrings.size(), pageIndex, artifactString); } @Override protected void done() { if (!isCancelled()) { try { ViewUpdate viewUpdate = get(); if (viewUpdate != null) { updateView(viewUpdate); } } catch (InterruptedException | ExecutionException ex) { logger.log(Level.WARNING, "Artifact display task unexpectedly interrupted or failed", ex); //NON-NLS } } } } }