/* * Autopsy Forensic Browser * * Copyright 2011-2016 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.Cursor; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.List; import javax.swing.JTabbedPane; import javax.swing.SwingUtilities; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import org.openide.explorer.ExplorerManager; import org.openide.nodes.Node; import org.openide.nodes.NodeEvent; import org.openide.nodes.NodeListener; import org.openide.nodes.NodeMemberEvent; import org.openide.nodes.NodeReorderEvent; import org.openide.util.Lookup; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.corecomponentinterfaces.DataContent; import org.sleuthkit.autopsy.corecomponentinterfaces.DataResult; import org.sleuthkit.autopsy.corecomponentinterfaces.DataResultViewer; import org.sleuthkit.autopsy.coreutils.Logger; /** * Data result panel component with its viewer tabs. * * The component is a generic JPanel and it can be reused in other swing * components or in a TopComponent. * * Use the static factory methods to instantiate and customize the component. * One option is to link a custom data content viewer to link to this viewer. * */ public class DataResultPanel extends javax.swing.JPanel implements DataResult, ChangeListener { private ExplorerManager explorerManager; private ExplorerManagerNodeSelectionListener emNodeSelectionListener; private Node rootNode; // Different DataResultsViewers private final List<UpdateWrapper> viewers = new ArrayList<>(); //custom content viewer to send selections to, or null if the main one private DataContent customContentViewer; private boolean isMain; private String title; private final RootNodeListener rootNodeListener = new RootNodeListener(); private static final Logger logger = Logger.getLogger(DataResultPanel.class.getName()); private boolean listeningToTabbedPane = false; private static final String DUMMY_NODE_DISPLAY_NAME = NbBundle.getMessage(DataResultPanel.class, "DataResultPanel.dummyNodeDisplayName"); /** * Creates new DataResultPanel Default constructor, needed mostly for the * palette/UI builder Use overrides or factory methods for more * customization. */ private DataResultPanel() { this.isMain = true; initComponents(); setName(title); this.title = ""; } /** * Creates data result panel * * @param isMain whether it is the main panel associated with the main * window, clients will almost always use false * @param title title string to be displayed */ DataResultPanel(boolean isMain, String title) { this(); setName(title); this.isMain = isMain; this.title = title; } /** * Create a new, custom data result panel, in addition to the application * main one and links with a custom data content panel. * * @param name unique name of the data result window, also * used as title * @param customContentViewer custom content viewer to send selection events * to */ DataResultPanel(String title, DataContent customContentViewer) { this(false, title); setName(title); //custom content viewer tc to setup for every result viewer this.customContentViewer = customContentViewer; } /** * Factory method to create, customize and open a new custom data result * panel. * * @param title Title of the result panel * @param pathText Descriptive text about the source of the nodes * displayed * @param givenNode The new root node * @param totalMatches Cardinality of root node's children * * @return a new DataResultPanel instance representing a custom data result * viewer */ public static DataResultPanel createInstance(String title, String pathText, Node givenNode, int totalMatches) { DataResultPanel newDataResult = new DataResultPanel(false, title); createInstanceCommon(pathText, givenNode, totalMatches, newDataResult); newDataResult.open(); return newDataResult; } /** * Factory method to create, customize and open a new custom data result * panel. * * @param title Title of the component window * @param pathText Descriptive text about the source of the nodes * displayed * @param givenNode The new root node * @param totalMatches Cardinality of root node's children * @param dataContent a handle to data content to send selection events to * * @return a new DataResultPanel instance representing a custom data result * viewer */ public static DataResultPanel createInstance(String title, String pathText, Node givenNode, int totalMatches, DataContent dataContent) { DataResultPanel newDataResult = new DataResultPanel(title, dataContent); createInstanceCommon(pathText, givenNode, totalMatches, newDataResult); newDataResult.open(); return newDataResult; } /** * Factory method to create, customize and open a new custom data result * panel. Does NOT call open(). Client must manually initialize by calling * open(). * * @param title Title of the component window * @param pathText Descriptive text about the source of the nodes * displayed * @param givenNode The new root node * @param totalMatches Cardinality of root node's children * @param dataContent a handle to data content to send selection events to * * @return a new DataResultPanel instance representing a custom data result * viewer */ public static DataResultPanel createInstanceUninitialized(String title, String pathText, Node givenNode, int totalMatches, DataContent dataContent) { DataResultPanel newDataResult = new DataResultPanel(title, dataContent); createInstanceCommon(pathText, givenNode, totalMatches, newDataResult); return newDataResult; } /** * Common code for factory helper methods * * @param pathText * @param givenNode * @param totalMatches * @param newDataResult */ private static void createInstanceCommon(String pathText, Node givenNode, int totalMatches, DataResultPanel newDataResult) { newDataResult.numberMatchLabel.setText(Integer.toString(totalMatches)); // set the tree table view newDataResult.setNode(givenNode); newDataResult.setPath(pathText); } /** * Sets content viewer to the custom one. Needs to be done before the first * call to open() * * @param customContentViewer */ public void setContentViewer(DataContent customContentViewer) { this.customContentViewer = customContentViewer; } /** * Initializes the panel internals and activates it. Call it within your top * component when it is opened. Do not use if used one of the factory * methods to create and open the component. */ public void open() { if (null == explorerManager) { // Get an ExplorerManager to pass to the child DataResultViewers. If the application // components are put together as expected, this will be an ExplorerManager owned // by an ancestor TopComponent. The TopComponent will have put this ExplorerManager // in a Lookup that is set as the action global context when the TopComponent has // focus. This makes Node selections available to Actions without coupling the // actions to a particular Component. Note that getting the ExplorerManager in the // constructor would be too soon, since the object has no ancestor TopComponent at // that point. explorerManager = ExplorerManager.find(this); // A DataResultPanel listens for Node selections in its DataResultViewers so it // can push the selections both to its child DataResultViewers and to a DataContent object. // The default DataContent object is a DataContentTopComponent in the data content mode (area), // and is the parent of a DataContentPanel that hosts a set of DataContentViewers. emNodeSelectionListener = new ExplorerManagerNodeSelectionListener(); explorerManager.addPropertyChangeListener(emNodeSelectionListener); } // Add all the DataContentViewer to the tabbed pannel. // (Only when the it's opened at the first time: tabCount = 0) int totalTabs = this.dataResultTabbedPanel.getTabCount(); if (totalTabs == 0) { // @@@ Restore the implementation of DataResultViewerTable and DataResultViewerThumbnail // as DataResultViewer service providers when DataResultViewers are updated // to better handle the ExplorerManager sharing implemented to support actions that operate on // multiple selected nodes. addDataResultViewer(new DataResultViewerTable(this.explorerManager)); addDataResultViewer(new DataResultViewerThumbnail(this.explorerManager)); // Find all DataResultViewer service providers and add them to the tabbed pane. for (DataResultViewer factory : Lookup.getDefault().lookupAll(DataResultViewer.class)) { // @@@ Revist this isMain condition, it may be obsolete. If not, // document the intent of DataResultViewer.createInstance() in the // DataResultViewer interface defintion. DataResultViewer drv; if (isMain) { //for main window, use the instance in the lookup drv = factory; } else { //create a new instance of the viewer for non-main window drv = factory.createInstance(); } addDataResultViewer(drv); } } if (isMain) { // if no node selected on DataExplorer, clear the field if (rootNode == null) { setNode(rootNode); } } this.setVisible(true); } private class ExplorerManagerNodeSelectionListener implements PropertyChangeListener { @Override public void propertyChange(PropertyChangeEvent evt) { if (!Case.isCaseOpen()) { // Handle the in-between condition when case is being closed // and legacy selection events are pumped. return; } if (evt.getPropertyName().equals(ExplorerManager.PROP_SELECTED_NODES)) { setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); // If a custom DataContent object has not been specified, get the default instance. DataContent contentViewer = customContentViewer; if (contentViewer == null) { contentViewer = Lookup.getDefault().lookup(DataContent.class); } try { if (contentViewer != null) { Node[] selectedNodes = explorerManager.getSelectedNodes(); for (UpdateWrapper drv : viewers) { drv.setSelectedNodes(selectedNodes); } // Passing null signals that either multiple nodes are selected, or no nodes are selected. // This is important to the DataContent object, since the content mode (area) of the app is designed // to show only the content underlying a single Node. if (selectedNodes.length == 1) { contentViewer.setNode(selectedNodes[0]); } else { contentViewer.setNode(null); } } } finally { setCursor(null); } } } } private void addDataResultViewer(DataResultViewer dataResultViewer) { UpdateWrapper viewerWrapper = new UpdateWrapper(dataResultViewer); if (null != this.customContentViewer) { viewerWrapper.setContentViewer(this.customContentViewer); } this.viewers.add(viewerWrapper); this.dataResultTabbedPanel.addTab(dataResultViewer.getTitle(), dataResultViewer.getComponent()); } /** * Tears down the component. Use within your outer container (such as a top * component) when it goes away to tear down this component and detach its * listeners. */ void close() { if (null != explorerManager && null != emNodeSelectionListener) { explorerManager.removePropertyChangeListener(emNodeSelectionListener); explorerManager = null; } // clear all set nodes for (UpdateWrapper drv : this.viewers) { drv.setNode(null); } if (!this.isMain) { for (UpdateWrapper drv : this.viewers) { drv.clearComponent(); } this.directoryTablePath.removeAll(); this.directoryTablePath = null; this.numberMatchLabel.removeAll(); this.numberMatchLabel = null; this.matchLabel.removeAll(); this.matchLabel = null; this.setLayout(null); this.removeAll(); this.setVisible(false); } } @Override public String getPreferredID() { return getName(); } @Override public void setNode(Node selectedNode) { if (this.rootNode != null) { this.rootNode.removeNodeListener(rootNodeListener); } // Deferring becoming a listener to the tabbed pane until this point // eliminates handling a superfluous stateChanged event during construction. if (listeningToTabbedPane == false) { dataResultTabbedPanel.addChangeListener(this); listeningToTabbedPane = true; } this.rootNode = selectedNode; if (this.rootNode != null) { rootNodeListener.reset(); this.rootNode.addNodeListener(rootNodeListener); } resetTabs(selectedNode); setupTabs(selectedNode); if (selectedNode != null) { int childrenCount = selectedNode.getChildren().getNodesCount(); this.numberMatchLabel.setText(Integer.toString(childrenCount)); } this.numberMatchLabel.setVisible(true); } private void setupTabs(Node selectedNode) { //update/disable tabs based on if supported for this node int drvC = 0; for (UpdateWrapper drv : viewers) { if (drv.isSupported(selectedNode)) { dataResultTabbedPanel.setEnabledAt(drvC, true); } else { dataResultTabbedPanel.setEnabledAt(drvC, false); } ++drvC; } // if the current tab is no longer enabled, then find one that is boolean hasViewerEnabled = true; int currentActiveTab = dataResultTabbedPanel.getSelectedIndex(); if ((currentActiveTab == -1) || (dataResultTabbedPanel.isEnabledAt(currentActiveTab) == false)) { hasViewerEnabled = false; for (int i = 0; i < dataResultTabbedPanel.getTabCount(); i++) { if (dataResultTabbedPanel.isEnabledAt(i)) { currentActiveTab = i; hasViewerEnabled = true; break; } } if (hasViewerEnabled) { dataResultTabbedPanel.setSelectedIndex(currentActiveTab); } } if (hasViewerEnabled) { viewers.get(currentActiveTab).setNode(selectedNode); } } @Override public void setTitle(String title) { setName(title); } @Override public void setPath(String pathText) { this.directoryTablePath.setText(pathText); } @Override public boolean isMain() { return this.isMain; } @Override public List<DataResultViewer> getViewers() { List<DataResultViewer> ret = new ArrayList<DataResultViewer>(); for (UpdateWrapper w : viewers) { ret.add(w.getViewer()); } return ret; } public boolean canClose() { return (!this.isMain) || !Case.isCaseOpen() || Case.getCurrentCase().hasData() == false; // only allow this window to be closed when there's no case opened or no image in this case } @Override public void stateChanged(ChangeEvent e) { JTabbedPane pane = (JTabbedPane) e.getSource(); // Get and set current selected tab int currentTab = pane.getSelectedIndex(); if (currentTab != -1) { UpdateWrapper drv = this.viewers.get(currentTab); // @@@ Restore commented out isOutDated() check after DataResultViewers are updated // to better handle the ExplorerManager sharing implemented to support actions that operate on // multiple selected nodes. //if (drv.isOutdated()) { // change the cursor to "waiting cursor" for this operation this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); try { drv.setNode(rootNode); } finally { this.setCursor(null); } //} } } /** * why does this take a Node as parameter and then ignore it? * * * * Resets the tabs based on the selected Node. If the selected node is null * or not supported, disable that tab as well. * * @param selectedNode the selected content Node */ public void resetTabs(Node selectedNode) { for (UpdateWrapper drv : this.viewers) { drv.resetComponent(); } } public void setSelectedNodes(Node[] selected) { for (UpdateWrapper drv : this.viewers) { drv.setSelectedNodes(selected); } } public Node getRootNode() { return this.rootNode; } /** * 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() { directoryTablePath = new javax.swing.JLabel(); numberMatchLabel = new javax.swing.JLabel(); matchLabel = new javax.swing.JLabel(); dataResultTabbedPanel = new javax.swing.JTabbedPane(); setMinimumSize(new java.awt.Dimension(0, 5)); setPreferredSize(new java.awt.Dimension(5, 5)); org.openide.awt.Mnemonics.setLocalizedText(directoryTablePath, org.openide.util.NbBundle.getMessage(DataResultPanel.class, "DataResultPanel.directoryTablePath.text")); // NOI18N directoryTablePath.setMinimumSize(new java.awt.Dimension(5, 14)); org.openide.awt.Mnemonics.setLocalizedText(numberMatchLabel, org.openide.util.NbBundle.getMessage(DataResultPanel.class, "DataResultPanel.numberMatchLabel.text")); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(matchLabel, org.openide.util.NbBundle.getMessage(DataResultPanel.class, "DataResultPanel.matchLabel.text")); // NOI18N dataResultTabbedPanel.setMinimumSize(new java.awt.Dimension(0, 5)); javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addComponent(directoryTablePath, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(numberMatchLabel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(matchLabel)) .addComponent(dataResultTabbedPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(numberMatchLabel) .addComponent(matchLabel)) .addComponent(directoryTablePath, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addGap(0, 0, 0) .addComponent(dataResultTabbedPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); }// </editor-fold>//GEN-END:initComponents // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JTabbedPane dataResultTabbedPanel; private javax.swing.JLabel directoryTablePath; private javax.swing.JLabel matchLabel; private javax.swing.JLabel numberMatchLabel; // End of variables declaration//GEN-END:variables private static class UpdateWrapper { private DataResultViewer wrapped; private boolean outdated; UpdateWrapper(DataResultViewer wrapped) { this.wrapped = wrapped; this.outdated = true; } DataResultViewer getViewer() { return wrapped; } void setNode(Node selectedNode) { this.wrapped.setNode(selectedNode); this.outdated = false; } void resetComponent() { this.wrapped.resetComponent(); this.outdated = true; } void clearComponent() { this.wrapped.clearComponent(); this.outdated = true; } boolean isOutdated() { return this.outdated; } void setSelectedNodes(Node[] selected) { this.wrapped.setSelectedNodes(selected); } boolean isSupported(Node selectedNode) { return this.wrapped.isSupported(selectedNode); } void setContentViewer(DataContent contentViewer) { this.wrapped.setContentViewer(contentViewer); } } /** * Set number of matches to be displayed in the top right * * @param numMatches */ public void setNumMatches(Integer numMatches) { if (this.numberMatchLabel != null) { this.numberMatchLabel.setText(Integer.toString(numMatches)); } } private class RootNodeListener implements NodeListener { private volatile boolean waitingForData = true; public void reset() { waitingForData = true; } @Override public void childrenAdded(final NodeMemberEvent nme) { Node[] delta = nme.getDelta(); updateMatches(); /* * There is a known issue in this code whereby we will only call * setupTabs() once even though childrenAdded could be called * multiple times. That means that each panel may not have access to * all of the children when they decide if they support the content */ if (waitingForData && containsReal(delta)) { waitingForData = false; if (SwingUtilities.isEventDispatchThread()) { setupTabs(nme.getNode()); } else { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { setupTabs(nme.getNode()); } }); } } } private boolean containsReal(Node[] delta) { for (Node n : delta) { if (!n.getDisplayName().equals(DUMMY_NODE_DISPLAY_NAME)) { return true; } } return false; } /** * Updates the Number of Matches label on the DataResultPanel. * */ private void updateMatches() { if (rootNode != null && rootNode.getChildren() != null) { setNumMatches(rootNode.getChildren().getNodesCount()); } } @Override public void childrenRemoved(NodeMemberEvent nme) { updateMatches(); } @Override public void childrenReordered(NodeReorderEvent nre) { } @Override public void nodeDestroyed(NodeEvent ne) { } @Override public void propertyChange(PropertyChangeEvent evt) { } } }