From 40b51e77e123b91ecc938f2afeac70b4445bf97f Mon Sep 17 00:00:00 2001 From: Richard Cordovano Date: Tue, 23 May 2017 18:29:30 -0400 Subject: [PATCH] ViewContextAction fixes --- .../corecomponents/DataResultPanel.java | 1025 +++++++++-------- .../corecomponents/DataResultViewerTable.java | 44 +- .../DataResultViewerThumbnail.java | 40 +- .../corecomponents/TableFilterChildren.java | 52 +- .../corecomponents/TableFilterNode.java | 78 +- .../datamodel/DisplayableItemNode.java | 28 +- .../sleuthkit/autopsy/datamodel/FileNode.java | 192 +-- .../datamodel/VirtualDirectoryNode.java | 2 +- .../directorytree/DataResultFilterNode.java | 28 + .../DirectoryTreeFilterNode.java | 36 +- .../DirectoryTreeTopComponent.java | 121 +- .../directorytree/ViewContextAction.java | 334 ++---- 12 files changed, 1016 insertions(+), 964 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultPanel.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultPanel.java index a45dac5f4d..c171d33144 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultPanel.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultPanel.java @@ -39,372 +39,392 @@ 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. + * A Swing JPanel with a JTabbedPane child component that contains result + * viewers (implementations of the DataResultViewer interface). The "main" + * DataResultPanel for the desktop application has a table viewer + * (DataResultViewerTable) and a thumbnail viewer (DataResultViewerThumbnail). + * The "main" panel and zero to many additional DataResultPanel instances are + * presented as tabs in the results view, the top component + * (DataResultTopComponent) normally docked into the upper right hand side of + * the main window of the desktop application. The result viewers in the "main + * panel" are used to view the child nodes of a node selected in the tree view + * (DirectoryTreeTopComponent) that is normally docked into the left hand side + * of the main window. * + * Nodes selected in the results view are displayed in a content view + * (implementation of the DataContent interface). The default content view is + * the DataContentTopComponent, normally docked into the lower right hand side + * of the main window. A custom content view may be specified instead. */ public class DataResultPanel extends javax.swing.JPanel implements DataResult, ChangeListener { + private static final long serialVersionUID = 1L; + private static final String PLEASE_WAIT_NODE_DISPLAY_NAME = NbBundle.getMessage(DataResultPanel.class, "DataResultPanel.pleasewaitNodeDisplayName"); + private final List resultViewers = new ArrayList<>(); private ExplorerManager explorerManager; private ExplorerManagerNodeSelectionListener emNodeSelectionListener; - private Node rootNode; - - // Different DataResultsViewers - private final List viewers = new ArrayList<>(); - //custom content viewer to send selections to, or null if the main one - private DataContent customContentViewer; + private final RootNodeListener rootNodeListener = new RootNodeListener(); + private DataContent customContentView; 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 PLEASEWAIT_NODE_DISPLAY_NAME = NbBundle.getMessage(DataResultPanel.class, - "DataResultPanel.pleasewaitNodeDisplayName"); + private boolean listeningToTabbedPane; /** - * Creates new DataResultPanel Default constructor, needed mostly for the - * palette/UI builder Use overrides or factory methods for more - * customization. + * Constructs and opens a Swing JPanel with a JTabbedPane child component + * that contains result viewers (implementations of the DataResultViewer + * interface). + * + * @param title The title for the panel. + * @param pathText Descriptive text about the source of the nodes + * displayed. + * @param rootNode The new root node. + * @param totalMatches Cardinality of root node's children + * + * @return A DataResultPanel instance. + */ + public static DataResultPanel createInstance(String title, String pathText, Node rootNode, int totalMatches) { + DataResultPanel newDataResult = new DataResultPanel(false, title); + createInstanceCommon(pathText, rootNode, totalMatches, newDataResult); + newDataResult.open(); + return newDataResult; + } + + /** + * Constructs and opens a Swing JPanel with a JTabbedPane child component + * that contains result viewers (implementations of the DataResultViewer + * interface). + * + * @param title The title for the panel. + * @param pathText Descriptive text about the source of the nodes + * displayed. + * @param rootNode The new root node. + * @param totalMatches Cardinality of root node's children + * @param customContentView A content view to use in place of the default + * content view. + * + * @return A DataResultPanel instance. + */ + public static DataResultPanel createInstance(String title, String pathText, Node rootNode, int totalMatches, DataContent customContentView) { + DataResultPanel newDataResult = new DataResultPanel(title, customContentView); + createInstanceCommon(pathText, rootNode, totalMatches, newDataResult); + newDataResult.open(); + return newDataResult; + } + + /** + * Constructs a Swing JPanel with a JTabbedPane child component that + * contains result viewers (implementations of the DataResultViewer + * interface). The panel is NOT opened; the client of this method must call + * open on the panel that is returned. + * + * @param title The title for the panel. + * @param pathText Descriptive text about the source of the nodes + * displayed. + * @param rootNode The new root node. + * @param totalMatches Cardinality of root node's children + * @param customContentView A content view to use in place of the default + * content view. + * + * @return A DataResultPanel instance. + */ + public static DataResultPanel createInstanceUninitialized(String title, String pathText, Node rootNode, int totalMatches, DataContent customContentView) { + DataResultPanel newDataResult = new DataResultPanel(title, customContentView); + createInstanceCommon(pathText, rootNode, totalMatches, newDataResult); + return newDataResult; + } + + /** + * Executes code common to all of the DataSreultPanel factory methods. + * + * @param pathText Descriptive text about the source of the nodes + * displayed. + * @param rootNode The new root node. + * @param totalMatches Cardinality of root node's children + * @param customContentView A content view to use in place of the default + * content view. + */ + private static void createInstanceCommon(String pathText, Node rootNode, int totalMatches, DataResultPanel customContentView) { + customContentView.numberMatchLabel.setText(Integer.toString(totalMatches)); + customContentView.setNode(rootNode); + customContentView.setPath(pathText); + } + + /** + * Constructs a Swing JPanel with a JTabbedPane child component that + * contains result viewers (implementations of the DataResultViewer + * interface). */ private DataResultPanel() { this.isMain = true; initComponents(); - - setName(title); - this.title = ""; + setName(title); } /** - * Creates data result panel + * Constructs a Swing JPanel with a JTabbedPane child component that + * contains result viewers (implementations of the DataResultViewer + * interface). * - * @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 + * @param isMain True if the DataResultPanel being constructed is the "main" + * DataResultPanel. + * @param title The title for the panel. */ DataResultPanel(boolean isMain, String title) { this(); - - setName(title); - this.isMain = isMain; this.title = title; + setName(title); } /** - * Create a new, custom data result panel, in addition to the application - * main one and links with a custom data content panel. + * Constructs a Swing JPanel with a JTabbedPane child component that + * contains result viewers (implementations of the DataResultViewer + * interface). * - * @param name unique name of the data result window, also - * used as title - * @param customContentViewer custom content viewer to send selection events - * to + * @param title The title for the panel. + * @param customContentView A content view to use in place of the default + * content view. */ - DataResultPanel(String title, DataContent customContentViewer) { + DataResultPanel(String title, DataContent customContentView) { this(false, title); setName(title); - - //custom content viewer tc to setup for every result viewer - this.customContentViewer = customContentViewer; + this.customContentView = customContentView; } /** - * Factory method to create, customize and open a new custom data result - * panel. + * Gets the preferred identifier for this panel in the window system. * - * @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 + * @return The preferred identifier. */ - 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) { - try { - Case.getCurrentCase(); - } catch (IllegalStateException ex) { - // 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(); } + /** + * Gets whether or not this panel is the "main" panel used to view the child + * nodes of a node selected in the tree view (DirectoryTreeTopComponent) + * that is normally docked into the left hand side of the main window. + * + * @return True or false. + */ @Override - public void setNode(Node selectedNode) { + public boolean isMain() { + return this.isMain; + } + + /** + * Sets the title of this panel. + * + * @param title The title. + */ + @Override + public void setTitle(String title) { + setName(title); + } + + /** + * Sets the descriptive text about the source of the nodes displayed in this + * panel. + * + * @param pathText The text to display. + */ + @Override + public void setPath(String pathText) { + this.directoryTablePath.setText(pathText); + } + + /** + * Sets the content view for this panel. Needs to be called before the first + * call to open. + * + * @param customContentView A content view to use in place of the default + * content view. + */ + public void setContentViewer(DataContent customContentView) { + this.customContentView = customContentView; + } + + /** + * Initializes this panel. Intended to be called by a parent top component + * when the top component is opened. + */ + public void open() { + if (null == explorerManager) { + /* + * Get an explorer manager to pass to the child result viewers. If + * the application components are put together as expected, this + * will be an explorer manager owned by a parent top component, and + * placed by the top component in the look up that is proxied as the + * action global context when the top component has focus. The + * sharing of this explorer manager enables the same child node + * selections to be made in all of the result viewers. + */ + explorerManager = ExplorerManager.find(this); + emNodeSelectionListener = new ExplorerManagerNodeSelectionListener(); + explorerManager.addPropertyChangeListener(emNodeSelectionListener); + } + + /* + * Load the child result viewers into the tabbed pane. + */ + if (0 == dataResultTabbedPanel.getTabCount()) { + /* + * TODO (JIRA-2658): Fix the DataResultViewer extension point. When + * this is done, restore the implementation of DataResultViewerTable + * and DataREsultViewerThumbnail as DataResultViewer service + * providers. + */ + addResultViewer(new DataResultViewerTable(this.explorerManager)); + addResultViewer(new DataResultViewerThumbnail(this.explorerManager)); + for (DataResultViewer factory : Lookup.getDefault().lookupAll(DataResultViewer.class)) { + DataResultViewer resultViewer; + if (isMain) { + resultViewer = factory; + } else { + resultViewer = factory.createInstance(); + } + addResultViewer(resultViewer); + } + } + + if (isMain && null == rootNode) { + setNode(rootNode); + } + + this.setVisible(true); + } + + /** + * Adds a result viewer to this panel. + * + * @param resultViewer The result viewer. + */ + private void addResultViewer(DataResultViewer resultViewer) { + if (null != customContentView) { + resultViewer.setContentViewer(customContentView); + } + resultViewers.add(resultViewer); + dataResultTabbedPanel.addTab(resultViewer.getTitle(), resultViewer.getComponent()); + } + + /** + * Gets the result viewers for this panel. + * + * @return A list of result viewers. + */ + @Override + public List getViewers() { + List viewers = new ArrayList<>(); + for (DataResultViewer viewer : resultViewers) { + viewers.add(viewer); + } + return viewers; + } + + /** + * Sets the root node for this panel. The child nodes of the root node will + * be displayed in the result viewers. For the "main" panel, the root node + * is the currently selected node in the tree view docked into the left side + * of the main application window. + * + * @param rootNode The root node for this panel. + */ + @Override + public void setNode(Node rootNode) { 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. + + /* + * 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; + this.rootNode = rootNode; if (this.rootNode != null) { rootNodeListener.reset(); this.rootNode.addNodeListener(rootNodeListener); } - resetTabs(selectedNode); - setupTabs(selectedNode); + resetTabs(this.rootNode); + setupTabs(this.rootNode); - if (selectedNode != null) { - int childrenCount = selectedNode.getChildren().getNodesCount(); + if (null != this.rootNode) { + int childrenCount = this.rootNode.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) { + /** + * Gets the root node of this panel. For the "main" panel, the root node is + * the currently selected node in the tree view docked into the left side of + * the main application window. + * + * @return The root node. + */ + public Node getRootNode() { + return rootNode; + } - if (drv.isSupported(selectedNode)) { - dataResultTabbedPanel.setEnabledAt(drvC, true); + /** + * Set number of child nodes displayed for the current root node. + * + * @param numberOfChildNodes + */ + public void setNumMatches(Integer numberOfChildNodes) { + if (this.numberMatchLabel != null) { + this.numberMatchLabel.setText(Integer.toString(numberOfChildNodes)); + } + } + + /** + * Sets the children of the root node that should be currently selected in + * this panel's result viewers. + * + * @param selectedNodes The nodes to be selected. + */ + public void setSelectedNodes(Node[] selectedNodes) { + for (DataResultViewer viewer : this.resultViewers) { + viewer.setSelectedNodes(selectedNodes); + } + } + + /** + * Sets the state of the child result viewers, based on a selected root + * node. + * + * @param selectedNode The selected node. + */ + private void setupTabs(Node selectedNode) { + /* + * Enable or disable the child tabs based on whether or not the + * corresponding results viewer supports display of the selected node. + */ + int tabIndex = 0; + for (DataResultViewer viewer : resultViewers) { + if (viewer.isSupported(selectedNode)) { + dataResultTabbedPanel.setEnabledAt(tabIndex, true); } else { - dataResultTabbedPanel.setEnabledAt(drvC, false); + dataResultTabbedPanel.setEnabledAt(tabIndex, false); } - ++drvC; + ++tabIndex; } - // if the current tab is no longer enabled, then find one that is + /* + * If the current tab is not enabled for the selected node, try to + * select a tab that is enabled. + */ boolean hasViewerEnabled = true; int currentActiveTab = dataResultTabbedPanel.getSelectedIndex(); if ((currentActiveTab == -1) || (dataResultTabbedPanel.isEnabledAt(currentActiveTab) == false)) { @@ -422,94 +442,234 @@ public class DataResultPanel extends javax.swing.JPanel implements DataResult, C } } - 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 getViewers() { - List ret = new ArrayList<>(); - for (UpdateWrapper w : viewers) { - ret.add(w.getViewer()); - } - - return ret; - } - - public boolean canClose() { /* - * If this is the main results panel in the main top component in the - * upper right of the main window, only allow it to be closed when - * there's no case opened or no data sources in the open case. + * Push the node to the selected results viewer. */ - return (!this.isMain) || !Case.isCaseOpen() || Case.getCurrentCase().hasData() == false; + if (hasViewerEnabled) { + resultViewers.get(currentActiveTab).setNode(selectedNode); + } + + /* + * Now that the selected node has been pushed to the selected results + * viewer and it has had an opportunity to act on the child selection + * info of the node, if any, clear the child selection info. + */ + ((TableFilterNode) selectedNode).setChildNodeSelectionInfo(null); } - @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); - } - //} + /** + * Resets the state of the child result viewers, based on a selected root + * node. + * + * @param unusedSelectedNode The selected node. + */ + public void resetTabs(Node unusedSelectedNode) { + for (DataResultViewer viewer : this.resultViewers) { + viewer.resetComponent(); } } /** - * why does this take a Node as parameter and then ignore it? + * Responds to a tab selection changed event by setting the root node of the + * corresponding result viewer. * - * - * - * 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 + * @param event The change event. */ - public void resetTabs(Node selectedNode) { - - for (UpdateWrapper drv : this.viewers) { - drv.resetComponent(); + @Override + public void stateChanged(ChangeEvent event) { + JTabbedPane pane = (JTabbedPane) event.getSource(); + int currentTab = pane.getSelectedIndex(); + if (-1 != currentTab) { + DataResultViewer currentViewer = this.resultViewers.get(currentTab); + this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + try { + currentViewer.setNode(rootNode); + } finally { + this.setCursor(null); + } } } - public void setSelectedNodes(Node[] selected) { - for (UpdateWrapper drv : this.viewers) { - drv.setSelectedNodes(selected); + /** + * Indicates whether or not this panel can be closed at the time of the + * call. + * + * @return True or false. + */ + public boolean canClose() { + /* + * If this is the "main" panel, only allow it to be closed when no case + * is open or no there are no data sources in the current case. + */ + return (!this.isMain) || !Case.isCaseOpen() || Case.getCurrentCase().hasData() == false; + } + + /** + * Closes down the component. Intended to be called by the parent top + * component when it is closed. + */ + void close() { + if (null != explorerManager && null != emNodeSelectionListener) { + explorerManager.removePropertyChangeListener(emNodeSelectionListener); + explorerManager = null; + } + + for (DataResultViewer viewer : this.resultViewers) { + viewer.setNode(null); + } + + if (!this.isMain) { + for (DataResultViewer viewer : this.resultViewers) { + viewer.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); } } - public Node getRootNode() { - return this.rootNode; + /** + * Responds to node selection change events from the explorer manager. + */ + private class ExplorerManagerNodeSelectionListener implements PropertyChangeListener { + + @Override + public void propertyChange(PropertyChangeEvent evt) { + try { + Case.getCurrentCase(); + } catch (IllegalStateException ex) { + return; + } + + /* + * Only interested in node selection events. + */ + if (evt.getPropertyName().equals(ExplorerManager.PROP_SELECTED_NODES)) { + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + + /* + * Use either the custom content view or the default view, if no + * custom view has been set. The default content view is the + * DataContentTopComponent docked into the lower right hand side + * of the main window of the application. + */ + DataContent contentViewer; + if (null != customContentView) { + contentViewer = customContentView; + } else { + contentViewer = Lookup.getDefault().lookup(DataContent.class); + } + + try { + if (contentViewer != null) { + Node[] selectedNodes = explorerManager.getSelectedNodes(); + + /* + * Pass the selected nodes to all of the result viewers + * sharing this explorer manager. + */ + resultViewers.forEach((viewer) -> { + viewer.setSelectedNodes(selectedNodes); + }); + + /* + * Passing null signals that either multiple nodes are + * selected, or no nodes are selected. This is important + * to the content view, since content views only work + * for a single node.. + */ + if (1 == selectedNodes.length) { + contentViewer.setNode(selectedNodes[0]); + } else { + contentViewer.setNode(null); + } + } + } finally { + setCursor(null); + } + } + } + } + + /** + * Responds to changes in the root node due to asynchronous child node + * creation. + */ + 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(PLEASE_WAIT_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) { + } } /** @@ -569,131 +729,4 @@ public class DataResultPanel extends javax.swing.JPanel implements DataResult, C 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(PLEASEWAIT_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) { - } - } } diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java index 407ec651f2..445261e06d 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java @@ -26,6 +26,7 @@ import java.awt.Graphics; import java.awt.dnd.DnDConstants; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.beans.PropertyVetoException; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; @@ -36,6 +37,7 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; +import java.util.logging.Level; import java.util.prefs.Preferences; import java.util.stream.Stream; import javax.swing.JTable; @@ -47,7 +49,6 @@ import javax.swing.event.TableColumnModelEvent; import javax.swing.event.TableColumnModelListener; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumnModel; -import org.apache.commons.lang3.StringUtils; import org.netbeans.swing.etable.ETableColumn; import org.netbeans.swing.outline.DefaultOutlineCellRenderer; import org.netbeans.swing.outline.DefaultOutlineModel; @@ -64,20 +65,22 @@ import org.openide.nodes.NodeMemberEvent; import org.openide.util.NbBundle; import org.openide.util.NbPreferences; import org.sleuthkit.autopsy.corecomponentinterfaces.DataResultViewer; +import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; +import org.sleuthkit.autopsy.datamodel.NodeSelectionInfo; /** - * DataResult sortable table viewer + * A tabular viewer for the results view. * - * @@@ Restore implementation of DataResultViewerTable as a DataResultViewer - * service provider when DataResultViewers can be made compatible with node - * multiple selection actions. + * TODO (JIRA-2658): Fix DataResultViewer extension point. When this is done, + * restore implementation of DataResultViewerTable as a DataResultViewer service + * provider. */ //@ServiceProvider(service = DataResultViewer.class) public class DataResultViewerTable extends AbstractDataResultViewer { private static final long serialVersionUID = 1L; - + private static final Logger logger = Logger.getLogger(DataResultViewerTable.class.getName()); @NbBundle.Messages("DataResultViewerTable.firstColLbl=Name") static private final String FIRST_COLUMN_LABEL = Bundle.DataResultViewerTable_firstColLbl(); @NbBundle.Messages("DataResultViewerTable.pleasewaitNodeDisplayName=Please Wait...") @@ -234,12 +237,26 @@ public class DataResultViewerTable extends AbstractDataResultViewer { oldNode.removeNodeListener(pleasewaitNodeListener); } - // if there's no selection node, do nothing if (hasChildren) { - Node root = selectedNode; + currentRoot = selectedNode; pleasewaitNodeListener.reset(); - root.addNodeListener(pleasewaitNodeListener); - setupTable(root); + currentRoot.addNodeListener(pleasewaitNodeListener); + setupTable(selectedNode); + NodeSelectionInfo selectedChildInfo = ((TableFilterNode) currentRoot).getChildNodeSelectionInfo(); + if (null != selectedChildInfo) { + Node[] childNodes = currentRoot.getChildren().getNodes(true); + for (int i = 0; i < childNodes.length; ++i) { + Node childNode = childNodes[i]; + if (selectedChildInfo.matches(childNode)) { + try { + em.setSelectedNodes(new Node[]{childNode}); + } catch (PropertyVetoException ex) { + logger.log(Level.SEVERE, "Failed to select node specified by selected child info", ex); + } + break; + } + } + } } else { Node emptyNode = new AbstractNode(Children.LEAF); em.setRootContext(emptyNode); // make empty node @@ -258,11 +275,8 @@ public class DataResultViewerTable extends AbstractDataResultViewer { * @param root The parent Node of the ContentNodes */ private void setupTable(final Node root) { - em.setRootContext(root); - currentRoot = root; - List> props = loadColumnOrder(); /** * OutlineView makes the first column be the result of @@ -275,10 +289,12 @@ public class DataResultViewerTable extends AbstractDataResultViewer { * duplicates getDisplayName(). The current implementation does not * allow the first property column to be moved. */ + List> props = loadColumnOrder(); if (props.isEmpty() == false) { Node.Property prop = props.remove(0); ((DefaultOutlineModel) outline.getOutlineModel()).setNodesColumnLabel(prop.getDisplayName()); } + /* * show the horizontal scroll panel and show all the content & header If * there is only one column (which was removed from props above) Just @@ -287,9 +303,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { outline.setAutoResizeMode((props.isEmpty()) ? JTable.AUTO_RESIZE_ALL_COLUMNS : JTable.AUTO_RESIZE_OFF); assignColumns(props); - setColumnWidths(); - loadColumnSorting(); } diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.java index 63d47f8a81..9a5edcb0a7 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.java @@ -48,22 +48,21 @@ import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.TskCoreException; /** - * Thumbnail view of images in data result with paging support. + * A thumbnail viewer for the results view, with paging support. * - * Paging is added to reduce memory footprint and load only up to (currently) - * 1000 images at a time. This works whether or not the underlying content nodes - * are being lazy loaded or not. + * The paging is intended to reduce memory footprint by load only up to + * (currently) 1000 images at a time. This works whether or not the underlying + * content nodes are being lazy loaded or not. * + * TODO (JIRA-2658): Fix DataResultViewer extension point. When this is done, + * restore implementation of DataResultViewerTable as a DataResultViewer service + * provider. */ -// @@@ Restore implementation of DataResultViewerThumbnail as a DataResultViewer -// service provider when DataResultViewers can be made compatible with node -// multi-selection actions. //@ServiceProvider(service = DataResultViewer.class) final class DataResultViewerThumbnail extends AbstractDataResultViewer { private static final long serialVersionUID = 1L; private static final Logger logger = Logger.getLogger(DataResultViewerThumbnail.class.getName()); - //flag to keep track if images are being loaded private int curPage; private int totalPages; private int curPageImages; @@ -71,8 +70,10 @@ final class DataResultViewerThumbnail extends AbstractDataResultViewer { private final PageUpdater pageUpdater = new PageUpdater(); /** - * Creates a DataResultViewerThumbnail object that is compatible with node - * multiple selection actions. + * Constructs a thumbnail viewer for the results view, with paging support, + * that is compatible with node multiple selection actions. + * + * @param explorerManager The shared ExplorerManager for the result viewers. */ DataResultViewerThumbnail(ExplorerManager explorerManager) { super(explorerManager); @@ -80,8 +81,8 @@ final class DataResultViewerThumbnail extends AbstractDataResultViewer { } /** - * Creates a DataResultViewerThumbnail object that is NOT compatible with - * node multiple selection actions. + * Constructs a thumbnail viewer for the results view, with paging support, + * that is NOT compatible with node multiple selection actions. */ DataResultViewerThumbnail() { initialize(); @@ -93,7 +94,6 @@ final class DataResultViewerThumbnail extends AbstractDataResultViewer { }) private void initialize() { initComponents(); - iconView.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); em.addPropertyChangeListener(new ExplorerManagerNodeSelectionListener()); thumbnailSizeComboBox.setModel(new javax.swing.DefaultComboBoxModel<>( @@ -311,20 +311,22 @@ final class DataResultViewerThumbnail extends AbstractDataResultViewer { @Override public void setNode(Node givenNode) { - // change the cursor to "waiting cursor" for this operation setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); try { if (givenNode != null) { + /* + * Wrap the given node in a ThumbnailViewChildren that will + * produce ThumbnailPageNodes with ThumbnailViewNode children + * from the child nodes of the given node. + */ ThumbnailViewChildren childNode = new ThumbnailViewChildren(givenNode, iconSize); - final Node root = new AbstractNode(childNode); pageUpdater.setRoot(root); root.addNodeListener(pageUpdater); em.setRootContext(root); } else { Node emptyNode = new AbstractNode(Children.LEAF); - em.setRootContext(emptyNode); // make empty node - + em.setRootContext(emptyNode); iconView.setBackground(Color.BLACK); } } finally { @@ -349,21 +351,18 @@ final class DataResultViewerThumbnail extends AbstractDataResultViewer { this.curPage = -1; curPageImages = 0; updateControls(); - } @Override public void clearComponent() { this.iconView.removeAll(); this.iconView = null; - super.clearComponent(); } private void nextPage() { if (curPage < totalPages) { curPage++; - switchPage(); } } @@ -371,7 +370,6 @@ final class DataResultViewerThumbnail extends AbstractDataResultViewer { private void previousPage() { if (curPage > 1) { curPage--; - switchPage(); } } diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/TableFilterChildren.java b/Core/src/org/sleuthkit/autopsy/corecomponents/TableFilterChildren.java index 753483e42c..6226b3e58b 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/TableFilterChildren.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/TableFilterChildren.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2016 Basis Technology Corp. + * Copyright 2011-2017 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,12 +23,35 @@ import org.openide.nodes.FilterNode; import org.openide.nodes.Node; /** - * A children (child factory) implementation for a TableFilterNode. A - * TableFilterNode creates at most one layer of child nodes for the node it - * wraps. It is designed to be used for nodes displayed in Autopsy table views. + * A Children implementation for a TableFilterNode. A TableFilterNode creates at + * most one layer of child nodes for the node it wraps. It is designed to be + * used in the results view to ensure the individual viewers display only the + * first layer of child nodes. */ class TableFilterChildren extends FilterNode.Children { + /** + * Creates a Children object for a TableFilterNode. A TableFilterNode + * creates at most one layer of child nodes for the node it wraps. It is + * designed to be used in the results view to ensure the individual viewers + * display only the first layer of child nodes. + * + * + * @param wrappedNode The node wrapped by the TableFilterNode. + * @param createChildren True if a children (child factory) object should be + * created for the wrapped node. + * + * @return A children (child factory) object for a node wrapped by a + * TableFilterNode. + */ + public static Children createInstance(Node wrappedNode, boolean createChildren) { + if (createChildren) { + return new TableFilterChildren(wrappedNode); + } else { + return Children.LEAF; + } + } + /** * Constructs a children (child factory) implementation for a * TableFilterNode. A TableFilterNode creates at most one layer of child @@ -67,25 +90,4 @@ class TableFilterChildren extends FilterNode.Children { return new Node[]{this.copyNode(key)}; } - /** - * Creates a children (child factory) object for a node wrapped in a - * TableFilterNode. A TableFilterNode creates at most one layer of child - * nodes for the node it wraps. It is designed to be used for nodes - * displayed in Autopsy table views. - * - * - * @param wrappedNode The node wrapped by the TableFilterNode. - * @param createChildren True if a children (child factory) object should be - * created for the wrapped node. - * - * @return A children (child factory) object for a node wrapped by a - * TableFilterNode. - */ - public static Children createInstance(Node wrappedNode, boolean createChildren) { - if (createChildren) { - return new TableFilterChildren(wrappedNode); - } else { - return Children.LEAF; - } - } } diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/TableFilterNode.java b/Core/src/org/sleuthkit/autopsy/corecomponents/TableFilterNode.java index 92fc5ce13e..2541c11277 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/TableFilterNode.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/TableFilterNode.java @@ -22,12 +22,13 @@ import org.openide.nodes.FilterNode; import org.openide.nodes.Node; import org.openide.util.NbBundle; import org.openide.util.lookup.Lookups; +import org.sleuthkit.autopsy.datamodel.NodeSelectionInfo; +import org.sleuthkit.autopsy.directorytree.DataResultFilterNode; /** * A filter node that creates at most one layer of child nodes for the node it - * wraps. It is designed to be used for nodes displayed in Autopsy table views. - * This ensures that the table view for the node will not recursively display - * child nodes and display only the first layer of child nodes. + * wraps. It is designed to be used in the results view to ensure the individual + * viewers display only the first layer of child nodes. */ public class TableFilterNode extends FilterNode { @@ -36,37 +37,40 @@ public class TableFilterNode extends FilterNode { /** * Constructs a filter node that creates at most one layer of child nodes - * for the node it wraps. It is designed to be used for nodes displayed in - * Autopsy table views. + * for the node it wraps. It is designed to be used in the results view to + * ensure the individual viewers display only the first layer of child + * nodes. * - * @param wrappedNode The node to wrap in the filter node. - * @param createChildren True if a children (child factory) object should be - * created for the wrapped node. - * The constructor should include column order key. (See getColumnOrderKey) + * @param node The node to wrap in the filter node. + * @param createChildren True if a Children object should be created for the + * wrapped node. */ - public TableFilterNode(Node wrappedNode, boolean createChildren) { - super(wrappedNode, TableFilterChildren.createInstance(wrappedNode, createChildren) , Lookups.proxy(wrappedNode)); + public TableFilterNode(Node node, boolean createChildren) { + super(node, TableFilterChildren.createInstance(node, createChildren), Lookups.proxy(node)); this.createChildren = createChildren; } /** - * Constructs a filter node that has information about the node's type. - * - * @param wrappedNode The node to wrap in the filter node. - * @param createChildren True if a children (child factory) object should be - * created for the wrapped node. + * Constructs a filter node that creates at most one layer of child nodes + * for the node it wraps. It is designed to be used in the results view to + * ensure the individual viewers display only the first layer of child + * nodes. + * + * @param node The node to wrap in the filter node. + * @param createChildren True if a Children object should be created for the + * wrapped node. * @param columnOrderKey A key that represents the type of the original * wrapped node and what is being displayed under that * node. */ - public TableFilterNode(Node wrappedNode, boolean createChildren, String columnOrderKey) { - super(wrappedNode, TableFilterChildren.createInstance(wrappedNode, createChildren)); + public TableFilterNode(Node node, boolean createChildren, String columnOrderKey) { + super(node, TableFilterChildren.createInstance(node, createChildren)); this.createChildren = createChildren; this.columnOrderKey = columnOrderKey; } /** - * Returns a display name for the wrapped node, for use in the first column + * Gets the display name for the wrapped node, for use in the first column * of an Autopsy table view. * * @return The display name. @@ -80,6 +84,41 @@ public class TableFilterNode extends FilterNode { } } + /** + * Adds information about which child node of this node, if any, should be + * selected. Can be null. + * + * @param selectedChildNodeInfo The child node selection information. + */ + public void setChildNodeSelectionInfo(NodeSelectionInfo selectedChildNodeInfo) { + /* + * Currently, child selection is only supported for nodes selected in + * the tree view and decorated with a DataResultFilterNode. + */ + if (getOriginal() instanceof DataResultFilterNode) { + ((DataResultFilterNode) getOriginal()).setChildNodeSelectionInfo(selectedChildNodeInfo); + } + } + + /** + * Gets information about which child node of this node, if any, should be + * selected. + * + * @return The child node selection information, or null if no child should + * be selected. + */ + public NodeSelectionInfo getChildNodeSelectionInfo() { + /* + * Currently, child selection is only supported for nodes selected in + * the tree view and decorated with a DataResultFilterNode. + */ + if (getOriginal() instanceof DataResultFilterNode) { + return ((DataResultFilterNode) getOriginal()).getChildNodeSelectionInfo(); + } else { + return null; + } + } + /** * @return the column order key, which allows custom column ordering to be * written into a properties file and be reloaded for future use in @@ -90,4 +129,5 @@ public class TableFilterNode extends FilterNode { String getColumnOrderKey() { return columnOrderKey; } + } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNode.java index 7229a1682c..33dc37e66f 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNode.java @@ -40,7 +40,7 @@ public abstract class DisplayableItemNode extends AbstractNode { * An item type shared by DisplayableItemNodes that can be the parents of * file nodes. */ - final static String FILE_PARENT_NODE_KEY = "orgsleuthkitautopsydatamodel" + "FileTypeParentNode"; + static final String FILE_PARENT_NODE_KEY = "orgsleuthkitautopsydatamodel" + "FileTypeParentNode"; /** * Gets the file, if any, linked to an artifact via a TSK_PATH_ID attribute @@ -62,6 +62,8 @@ public abstract class DisplayableItemNode extends AbstractNode { return null; } + private NodeSelectionInfo selectedChildNodeInfo; + /** * Constructs a node that is eligible for display in the tree view or * results view. Capabilitites include accepting a @@ -102,7 +104,8 @@ public abstract class DisplayableItemNode extends AbstractNode { public abstract T accept(DisplayableItemNodeVisitor visitor); /** - * Indicates whether or not the node is a leaf node. + * Indicates whether or not the node is capable of having child nodes. + * Should only return true if the node is ALWAYS a leaf node. * * @return True or false. */ @@ -115,4 +118,25 @@ public abstract class DisplayableItemNode extends AbstractNode { */ public abstract String getItemType(); + /** + * Adds information about which child node of this node, if any, should be + * selected. Can be null. + * + * @param selectedChildNodeInfo The child node selection information. + */ + public void setChildNodeSelectionInfo(NodeSelectionInfo selectedChildNodeInfo) { + this.selectedChildNodeInfo = selectedChildNodeInfo; + } + + /** + * Gets information about which child node of this node, if any, should be + * selected. + * + * @return The child node selection information, or null if no child should + * be selected. + */ + public NodeSelectionInfo getChildNodeSelectionInfo() { + return selectedChildNodeInfo; + } + } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java index 10ba139efd..69932e6e2a 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java @@ -19,6 +19,7 @@ package org.sleuthkit.autopsy.datamodel; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -30,7 +31,6 @@ import org.sleuthkit.autopsy.actions.AddContentTagAction; import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; import org.sleuthkit.autopsy.coreutils.ImageUtils; -import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; import org.sleuthkit.autopsy.directorytree.HashSearchAction; @@ -42,32 +42,84 @@ import org.sleuthkit.datamodel.TskData.TSK_DB_FILES_TYPE_ENUM; import org.sleuthkit.datamodel.TskData.TSK_FS_NAME_FLAG_ENUM; /** - * This class is the Node for an AbstractFile. It may have derived files + * A node for representing an AbstractFile. It may have derived file node * children. */ public class FileNode extends AbstractFsContentNode { - - private static final Logger LOGGER = Logger.getLogger(FileNode.class.getName()); /** - * Constructor + * Gets the path to the icon file that should be used to visually represent + * an AbstractFile, using the file name extension to select the icon. * - * @param file underlying Content + * @param file An AbstractFile. + * + * @return An icon file path. + */ + static String getIconForFileType(AbstractFile file) { + String ext = file.getNameExtension(); + if (StringUtils.isBlank(ext)) { + return "org/sleuthkit/autopsy/images/file-icon.png"; //NON-NLS + } else { + ext = "." + ext; + } + if (ImageUtils.isImageThumbnailSupported(file) + || FileTypeExtensions.getImageExtensions().contains(ext)) { + return "org/sleuthkit/autopsy/images/image-file.png"; //NON-NLS + } + if (FileTypeExtensions.getVideoExtensions().contains(ext)) { + return "org/sleuthkit/autopsy/images/video-file.png"; //NON-NLS + } + if (FileTypeExtensions.getAudioExtensions().contains(ext)) { + return "org/sleuthkit/autopsy/images/audio-file.png"; //NON-NLS + } + if (FileTypeExtensions.getDocumentExtensions().contains(ext)) { + return "org/sleuthkit/autopsy/images/doc-file.png"; //NON-NLS + } + if (FileTypeExtensions.getExecutableExtensions().contains(ext)) { + return "org/sleuthkit/autopsy/images/exe-file.png"; //NON-NLS + } + if (FileTypeExtensions.getTextExtensions().contains(ext)) { + return "org/sleuthkit/autopsy/images/text-file.png"; //NON-NLS + } + if (FileTypeExtensions.getWebExtensions().contains(ext)) { + return "org/sleuthkit/autopsy/images/web-file.png"; //NON-NLS + } + if (FileTypeExtensions.getPDFExtensions().contains(ext)) { + return "org/sleuthkit/autopsy/images/pdf-file.png"; //NON-NLS + } + if (FileTypeExtensions.getArchiveExtensions().contains(ext)) { + return "org/sleuthkit/autopsy/images/archive-file.png"; //NON-NLS + } + return "org/sleuthkit/autopsy/images/file-icon.png"; //NON-NLS + } + + /** + * Constructs a node for representing an AbstractFile. It may have derived + * file node children. + * + * @param file An AbstractFile object. */ public FileNode(AbstractFile file) { this(file, true); - setIcon(file); } + /** + * Constructs a node for representing an AbstractFile. It may have derived + * file node children. + * + * @param file An AbstractFile object. + * @param directoryBrowseMode + */ public FileNode(AbstractFile file, boolean directoryBrowseMode) { super(file, directoryBrowseMode); - setIcon(file); } + /* + * Sets the icon for the node, based on properties of the AbstractFile. + */ private void setIcon(AbstractFile file) { - // set name, display name, and icon if (file.isDirNameFlagSet(TSK_FS_NAME_FLAG_ENUM.UNALLOC)) { if (file.getType().equals(TSK_DB_FILES_TYPE_ENUM.CARVED)) { this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/carved-file-icon-16.png"); //NON-NLS @@ -79,110 +131,92 @@ public class FileNode extends AbstractFsContentNode { } } + /** + * Gets the set of actions that are associated with this node. This set is + * used to construct the context menu for the node. + * + * @param context Whether to find actions for context meaning or for the + * node itself. + * + * @return An array of the actions. + */ @Override @NbBundle.Messages({ "FileNode.getActions.viewFileInDir.text=View File in Directory", "FileNode.getActions.viewInNewWin.text=View in New Window", "FileNode.getActions.openInExtViewer.text=Open in External Viewer", "FileNode.getActions.searchFilesSameMD5.text=Search for files with the same MD5 hash"}) - public Action[] getActions(boolean popup) { + public Action[] getActions(boolean context) { List actionsList = new ArrayList<>(); - for (Action a : super.getActions(true)) { - actionsList.add(a); - } + actionsList.addAll(Arrays.asList(super.getActions(true))); if (!this.getDirectoryBrowseMode()) { actionsList.add(new ViewContextAction(Bundle.FileNode_getActions_viewFileInDir_text(), this)); - actionsList.add(null); // creates a menu separator + actionsList.add(null); // Creates a item separator } + actionsList.add(new NewWindowViewAction(Bundle.FileNode_getActions_viewInNewWin_text(), this)); actionsList.add(new ExternalViewerAction(Bundle.FileNode_getActions_openInExtViewer_text(), this)); actionsList.add(ViewFileInTimelineAction.createViewFileAction(getContent())); + actionsList.add(null); // Creates a item separator - actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); actionsList.add(new HashSearchAction(Bundle.FileNode_getActions_searchFilesSameMD5_text(), this)); - actionsList.add(null); // creates a menu separator + actionsList.add(null); // Creates a item separator + actionsList.add(AddContentTagAction.getInstance()); - final Collection selectedFilesList = new HashSet<>(Utilities.actionsGlobalContext().lookupAll(AbstractFile.class)); - if(selectedFilesList.size() == 1) { + if (1 == selectedFilesList.size()) { actionsList.add(DeleteFileContentTagAction.getInstance()); } - actionsList.addAll(ContextMenuExtensionPoint.getActions()); return actionsList.toArray(new Action[actionsList.size()]); } + /** + * Accepts a ContentNodeVisitor. + * + * @param The type parameter of the Visitor. + * @param visitor The Visitor. + * + * @return An object determied by the type parameter of the Visitor. + */ @Override - public T accept(ContentNodeVisitor v) { - return v.visit(this); + public T accept(ContentNodeVisitor visitor) { + return visitor.visit(this); } + /** + * Accepts a DisplayableItemNodeVisitor. + * + * @param The type parameter of the Visitor. + * @param visitor The Visitor. + * + * @return An object determied by the type parameter of the Visitor. + */ @Override - public T accept(DisplayableItemNodeVisitor v) { - return v.visit(this); - } - - // Given a file, returns the correct icon for said - // file based off it's extension - static String getIconForFileType(AbstractFile file) { - // Get the name, extension - String ext = file.getNameExtension(); - - if (StringUtils.isBlank(ext)) { - return "org/sleuthkit/autopsy/images/file-icon.png"; //NON-NLS - } else { - ext = "." + ext; - } - - if (ImageUtils.isImageThumbnailSupported(file) - || FileTypeExtensions.getImageExtensions().contains(ext)) { - return "org/sleuthkit/autopsy/images/image-file.png"; //NON-NLS - } - // Videos - if (FileTypeExtensions.getVideoExtensions().contains(ext)) { - return "org/sleuthkit/autopsy/images/video-file.png"; //NON-NLS - } - // Audio Files - if (FileTypeExtensions.getAudioExtensions().contains(ext)) { - return "org/sleuthkit/autopsy/images/audio-file.png"; //NON-NLS - } - // Documents - if (FileTypeExtensions.getDocumentExtensions().contains(ext)) { - return "org/sleuthkit/autopsy/images/doc-file.png"; //NON-NLS - } - // Executables / System Files - if (FileTypeExtensions.getExecutableExtensions().contains(ext)) { - return "org/sleuthkit/autopsy/images/exe-file.png"; //NON-NLS - } - // Text Files - if (FileTypeExtensions.getTextExtensions().contains(ext)) { - return "org/sleuthkit/autopsy/images/text-file.png"; //NON-NLS - } - // Web Files - if (FileTypeExtensions.getWebExtensions().contains(ext)) { - return "org/sleuthkit/autopsy/images/web-file.png"; //NON-NLS - } - // PDFs - if (FileTypeExtensions.getPDFExtensions().contains(ext)) { - return "org/sleuthkit/autopsy/images/pdf-file.png"; //NON-NLS - } - // Archives - if (FileTypeExtensions.getArchiveExtensions().contains(ext)) { - return "org/sleuthkit/autopsy/images/archive-file.png"; //NON-NLS - } - // Else return the default - return "org/sleuthkit/autopsy/images/file-icon.png"; //NON-NLS + public T accept(DisplayableItemNodeVisitor visitor) { + return visitor.visit(this); } + /** + * Indicates whether or not the node is capable of having child nodes. + * Should only return true if the node is ALWAYS a leaf node. + * + * @return True or false. + */ @Override public boolean isLeafTypeNode() { - // This seems wrong, but it also seems that it is never called - // because the visitor to figure out if there are children or - // not will check if it has children using the Content API - return true; + /* + * A FileNode may have FileNodes for derived files as children. + */ + return false; } + /** + * Gets the item type string of the node, suitable for use as a key. + * + * @return A String representing the item type of node. + */ @Override public String getItemType() { return getClass().getName(); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/VirtualDirectoryNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/VirtualDirectoryNode.java index 8a056b2318..ac861c8847 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/VirtualDirectoryNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/VirtualDirectoryNode.java @@ -189,7 +189,7 @@ public class VirtualDirectoryNode extends AbstractAbstractFileNode { + // change the cursor to "waiting cursor" for this operation + DirectoryTreeTopComponent.this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + try { + Node treeNode = DirectoryTreeTopComponent.this.getSelectedNode(); + if (treeNode != null) { + Node originNode = ((DirectoryTreeFilterNode) treeNode).getOriginal(); + //set node, wrap in filter node first to filter out children + Node drfn = new DataResultFilterNode(originNode, DirectoryTreeTopComponent.this.em); + // Create a TableFilterNode with knowledge of the node's type to allow for column order settings + if (FileTypesByMimeType.isEmptyMimeTypeNode(originNode)) { + //Special case for when File Type Identification has not yet been run and + //there are no mime types to populate Files by Mime Type Tree + EmptyNode emptyNode = new EmptyNode(Bundle.DirectoryTreeTopComponent_emptyMimeNode_text()); + dataResult.setNode(new TableFilterNode(emptyNode, true, "This Node Is Empty")); //NON-NLS + } else if (originNode instanceof DisplayableItemNode) { + dataResult.setNode(new TableFilterNode(drfn, true, ((DisplayableItemNode) originNode).getItemType())); + } else { + dataResult.setNode(new TableFilterNode(drfn, true)); } - - // set the directory listing to be active - if (oldNodes != null && newNodes != null - && (oldNodes.length == newNodes.length)) { - boolean sameNodes = true; - for (int i = 0; i < oldNodes.length; i++) { - sameNodes = sameNodes && oldNodes[i].getName().equals(newNodes[i].getName()); - } - if (!sameNodes) { - dataResult.requestActive(); + String displayName = ""; + Content content = originNode.getLookup().lookup(Content.class); + if (content != null) { + try { + displayName = content.getUniquePath(); + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "Exception while calling Content.getUniquePath() for node: {0}", originNode); //NON-NLS } + } else if (originNode.getLookup().lookup(String.class) != null) { + displayName = originNode.getLookup().lookup(String.class); } - } finally { - setCursor(null); + dataResult.setPath(displayName); } + // set the directory listing to be active + if (oldNodes != null && newNodes != null + && (oldNodes.length == newNodes.length)) { + boolean sameNodes = true; + for (int i = 0; i < oldNodes.length; i++) { + sameNodes = sameNodes && oldNodes[i].getName().equals(newNodes[i].getName()); + } + if (!sameNodes) { + dataResult.requestActive(); + } + } + } finally { + setCursor(null); } }); @@ -774,29 +759,16 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat private void refreshDataSourceTree() { Node selectedNode = getSelectedNode(); final String[] selectedPath = NodeOp.createPath(selectedNode, em.getRootContext()); - Children rootChildren = em.getRootContext().getChildren(); Node dataSourcesFilterNode = rootChildren.findChild(DataSourcesNode.NAME); if (dataSourcesFilterNode == null) { LOGGER.log(Level.SEVERE, "Cannot find data sources filter node, won't refresh the content tree"); //NON-NLS return; } - DirectoryTreeFilterNode.OriginalNode imagesNodeOrig = dataSourcesFilterNode.getLookup().lookup(DirectoryTreeFilterNode.OriginalNode.class); - - if (imagesNodeOrig == null) { - LOGGER.log(Level.SEVERE, "Cannot find data sources node, won't refresh the content tree"); //NON-NLS - return; - } - - Node imagesNode = imagesNodeOrig.getNode(); - - DataSourcesNode.DataSourcesNodeChildren contentRootChildren = (DataSourcesNode.DataSourcesNodeChildren) imagesNode.getChildren(); + Node dataSourcesNode = ((DirectoryTreeFilterNode) dataSourcesFilterNode).getOriginal(); + DataSourcesNode.DataSourcesNodeChildren contentRootChildren = (DataSourcesNode.DataSourcesNodeChildren) dataSourcesNode.getChildren(); contentRootChildren.refreshContentKeys(); - - //final TreeView tree = getTree(); - //tree.expandNode(imagesNode); setSelectedNode(selectedPath, DataSourcesNode.NAME); - } /** @@ -963,6 +935,7 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat }); } + // RJCTODO: What is this all about? @Override public void viewArtifactContent(BlackboardArtifact art) { new ViewContextAction( diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ViewContextAction.java b/Core/src/org/sleuthkit/autopsy/directorytree/ViewContextAction.java index 5b11b4c33d..dc11c7f736 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/ViewContextAction.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ViewContextAction.java @@ -24,49 +24,46 @@ import java.beans.PropertyVetoException; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.concurrent.CancellationException; -import java.util.concurrent.ExecutionException; import java.util.logging.Level; import org.sleuthkit.autopsy.coreutils.Logger; import javax.swing.AbstractAction; -import javax.swing.SwingWorker; import org.openide.nodes.AbstractNode; import org.openide.explorer.ExplorerManager; import org.openide.explorer.view.TreeView; import org.openide.nodes.Children; import org.openide.nodes.Node; -import org.sleuthkit.autopsy.corecomponents.DataResultTopComponent; +import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.datamodel.AbstractFsContentNode; import org.sleuthkit.autopsy.datamodel.BlackboardArtifactNode; +import org.sleuthkit.autopsy.datamodel.ContentNodeSelectionInfo; import org.sleuthkit.autopsy.datamodel.DataSourcesNode; +import org.sleuthkit.autopsy.datamodel.DisplayableItemNode; import org.sleuthkit.autopsy.datamodel.RootContentChildren; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.ContentVisitor; -import org.sleuthkit.datamodel.Directory; import org.sleuthkit.datamodel.FileSystem; import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.TskData; import org.sleuthkit.datamodel.VolumeSystem; /** * An action that displays the context for some content by expanding the data * sources branch of the tree view to the level of the parent of the content, * selecting the parent in the tree view, then selecting the content in the - * results view. This is commonly called "view file in directory." + * results view. */ public final class ViewContextAction extends AbstractAction { private static final long serialVersionUID = 1L; - private static final long ROOT_DIR_META_ADDR = 5L; private static final Logger logger = Logger.getLogger(ViewContextAction.class.getName()); - private Content content; + private final Content content; /** * An action that displays the context for the source content of an artifact * by expanding the data sources branch of the tree view to the level of the * parent of the content, selecting the parent in the tree view, then - * selecting the content in the results view. This is commonly called "view - * file in directory." + * selecting the content in the results view. * * @param displayName The display name for the action. * @param artifactNode The artifact node for the artifact. @@ -74,14 +71,20 @@ public final class ViewContextAction extends AbstractAction { public ViewContextAction(String displayName, BlackboardArtifactNode artifactNode) { super(displayName); this.content = artifactNode.getLookup().lookup(Content.class); + if (content instanceof AbstractFile) { + AbstractFile file = (AbstractFile) content; + if ((TskData.FileKnown.KNOWN == file.getKnown() && UserPreferences.hideKnownFilesInDataSourcesTree()) + || (TskData.TSK_DB_FILES_TYPE_ENUM.SLACK == file.getType() && UserPreferences.hideSlackFilesInDataSourcesTree())) { + this.setEnabled(false); + } + } } /** * An action that displays the context for some file system content by * expanding the data sources branch of the tree view to the level of the * parent of the content, selecting the parent in the tree view, then - * selecting the content in the results view. This is commonly called "view - * file in directory." + * selecting the content in the results view. * * @param displayName The display name for the action. * @param fileSystemContentNode The file system content node for the @@ -96,8 +99,7 @@ public final class ViewContextAction extends AbstractAction { * An action that displays the context for some content by expanding the * data sources branch of the tree view to the level of the parent of the * content, selecting the parent in the tree view, then selecting the - * content in the results view. This is commonly called "view file in - * directory." + * content in the results view. * * @param displayName The display name for the action. * @param content The content. @@ -108,259 +110,153 @@ public final class ViewContextAction extends AbstractAction { } /** - * Invoked when the action occurs. + * Displays the context for some content by expanding the data sources + * branch of the tree view to the level of the parent of the content, + * selecting the parent in the tree view, then selecting the content in the + * results view. * - * @param event + * @param event The action event. */ @Override public void actionPerformed(ActionEvent event) { - /* - * Ensure that the action manipulates the tree view in the GUI event - * thread. - */ EventQueue.invokeLater(() -> { /* - * Create a flattened copy of the branch of the tree view that leads - * to the specified content, starting with the data source of the - * content, which actually could be the content itself. Note that - * the "dummy" root node used to create the branch needs to be - * wrapped in a DirectoryTreeFilterNode so that its child nodes will - * also be wrapped in DirectoryTreeFilterNodes via - * DirectoryTreeFilterNodeChildren. Otherwise, the display names of - * the nodes in the branch will not have child node counts, and will - * not match the display names of the corresponding nodes in the - * actual tree view. - */ - LeafToRootContentBranchVisitor branchBuilder = new LeafToRootContentBranchVisitor(); - List branch = content.accept(branchBuilder); - Collections.reverse(branch); - Node dummyRootNode = new DirectoryTreeFilterNode(new AbstractNode(new RootContentChildren(branch)), true); - Children branchChildren = dummyRootNode.getChildren(); - - /* - * Use the flattened copy of the branch of the tree view that leads - * to the specified content to do a depth-first search for the - * parent node of the content in the actual tree view, starting from - * the tree view's "Data Sources" node. + * Get the "Data Sources" node from the tree view. */ DirectoryTreeTopComponent treeViewTopComponent = DirectoryTreeTopComponent.findInstance(); - TreeView treeView = treeViewTopComponent.getTree(); ExplorerManager treeViewExplorerMgr = treeViewTopComponent.getExplorerManager(); - Node treeViewRootNode = treeViewExplorerMgr.getRootContext(); - Children treeViewRootNodeChildren = treeViewRootNode.getChildren(); - Node dataSourcesNode = treeViewRootNodeChildren.findChild(DataSourcesNode.NAME); - Children currentTreeViewNodeChildren = dataSourcesNode.getChildren(); - Node contentParentNode = null; - for (int i = 0; i < branchChildren.getNodesCount(); i++) { - Node currentBranchNode = branchChildren.getNodeAt(i); - for (int j = 0; j < currentTreeViewNodeChildren.getNodesCount(); j++) { - Node currentTreeViewNode = currentTreeViewNodeChildren.getNodeAt(j); - if (currentBranchNode.getDisplayName().equals(currentTreeViewNode.getDisplayName())) { - contentParentNode = currentTreeViewNode; - treeView.expandNode(contentParentNode); - currentTreeViewNodeChildren = currentTreeViewNode.getChildren(); - break; + Node parentTreeViewNode = treeViewExplorerMgr.getRootContext().getChildren().findChild(DataSourcesNode.NAME); + + /* + * Get the parent content for the content to be selected in the + * results view. If the parent content is null, then the specified + * content is a data source, and the parent tree view node is the + * "Data Sources" node. Otherwise, the tree view needs to be + * searched to find the parent treeview node. + */ + Content parentContent = null; + try { + parentContent = content.getParent(); + } catch (TskCoreException ex) { + // RJCTODO: Pop up + logger.log(Level.SEVERE, String.format("Could not get parent of Content object: %s", content), ex); //NON-NLS + return; + } + if (null != parentContent) { + /* + * Get an ordered list of the ancestors of the specified + * content, starting with its data source. + * + */ + AncestorVisitor ancestorVisitor = new AncestorVisitor(); + List contentBranch = parentContent.accept(ancestorVisitor); + Collections.reverse(contentBranch); + + /** + * Convert the list of ancestors into a list of tree nodes. + * + * IMPORTANT: The "dummy" root node used to create this single + * layer of children needs to be wrapped in a + * DirectoryTreeFilterNode so that its child nodes will also be + * wrapped in DirectoryTreeFilterNodes, via + * DirectoryTreeFilterNodeChildren. Otherwise, the display names + * of the nodes in the branch will not have child node counts + * and will not match the display names of the corresponding + * nodes in the actual tree view. + */ + Node dummyRootNode = new DirectoryTreeFilterNode(new AbstractNode(new RootContentChildren(contentBranch)), true); + Children ancestorChildren = dummyRootNode.getChildren(); + + /* + * Search the tree for the parent node. Note that this algorithm + * simply discards "extra" ancestor nodes not shown in the tree, + * such as the root directory of the file system for file system + * content. + */ + Children treeNodeChildren = parentTreeViewNode.getChildren(); + for (int i = 0; i < ancestorChildren.getNodesCount(); i++) { + Node ancestorNode = ancestorChildren.getNodeAt(i); + for (int j = 0; j < treeNodeChildren.getNodesCount(); j++) { + Node treeNode = treeNodeChildren.getNodeAt(j); + if (ancestorNode.getDisplayName().equals(treeNode.getDisplayName())) { + parentTreeViewNode = treeNode; + treeNodeChildren = treeNode.getChildren(); + break; + } } } } - if (null == contentParentNode) { - logger.log(Level.SEVERE, "Failed to find the parent node of Content node to be selected in the results view in the tree view"); //NON-NLS - return; - } - - if (branchChildren.getNodesCount() != 1) { - DirectoryTreeFilterNode contentParentFilterNode = (DirectoryTreeFilterNode) contentParentNode; - DirectoryTreeFilterNode.OriginalNode decoratedNodeHolder = contentParentFilterNode.getLookup().lookup(DirectoryTreeFilterNode.OriginalNode.class); - if (decoratedNodeHolder == null) { - logger.log(Level.SEVERE, "Failed to extract decorated node holder of the DirectoryTreeFilterNode decorator of the parent node of Content node to be selected in the results view"); //NON-NLS - return; - } - Node originNode = decoratedNodeHolder.getNode(); - } /* - * Select the parent node of content node to be selected in the - * results view in the tree view. + * Set the child selection info of the parent tree node, then select + * the parent node in the tree view. The results view will retrieve + * this selection info and use it to complete this action when the + * tree view top component responds to the selection of the parent + * node by pushing it into the results view top component. */ + DisplayableItemNode undecoratedParentNode = (DisplayableItemNode) ((DirectoryTreeFilterNode) parentTreeViewNode).getOriginal(); + undecoratedParentNode.setChildNodeSelectionInfo(new ContentNodeSelectionInfo(content)); + TreeView treeView = treeViewTopComponent.getTree(); + treeView.expandNode(parentTreeViewNode); try { - treeView.expandNode(contentParentNode); - treeViewExplorerMgr.setExploredContextAndSelection(contentParentNode, new Node[]{contentParentNode}); + // RJCTODO: Pop up + treeViewExplorerMgr.setExploredContextAndSelection(parentTreeViewNode, new Node[]{parentTreeViewNode}); } catch (PropertyVetoException ex) { - logger.log(Level.SEVERE, "Failed to select the parent node of Content node to be selected in the results view in the tree view", ex); //NON-NLS - return; - } - - if (branchChildren.getNodesCount() != 1) { - /* - * The target content is not a data source, queue another task - * for the GUI event thread to get the current root node of the - * result view top component, get the display name of the - * content node to be selected in the results view, and then - * dispatch a ResultViewNodeSelectionTask (a SwingWorker). - * - * TODO (JIRA-1655): This participates in a race condition. - */ -// EventQueue.invokeLater(() -> { -// DataResultTopComponent resultViewTopComponent = treeViewTopComponent.getDirectoryListing(); -// Node currentRootNodeOfResultsView = resultViewTopComponent.getRootNode(); -// Node contentNode = content.accept(new RootContentChildren.CreateSleuthkitNodeVisitor()); -// new ResultViewNodeSelectionTask(resultViewTopComponent, contentNode.getName(), currentRootNodeOfResultsView).execute(); -// }); + logger.log(Level.SEVERE, "Failed to select the parent node in the tree view", ex); //NON-NLS } }); } /** - * Makes a clone of this action. - * - * @return The cloned action. - * - * @throws CloneNotSupportedException Exception thrown if there is a problem - * creating the clone. + * A ContentVisitor that returns a list of content objects by starting with + * a given content and following its chain of ancestors to the root content + * of the lineage. */ - @Override - public Object clone() throws CloneNotSupportedException { - ViewContextAction clone = (ViewContextAction) super.clone(); - clone.setContent(this.content); - return clone; - } + private static class AncestorVisitor extends ContentVisitor.Default> { - /** - * Sets the content object associated with the action. - * - * @param content A content object. - */ - private void setContent(Content content) { - this.content = content; - } - - /** - * Waits for a Node's children to be generated, regardless of whether they - * are lazily loaded, then sets the correct selection in a specified - * DataResultTopComponent. - */ - private class ResultViewNodeSelectionTask extends SwingWorker { - - DataResultTopComponent resultViewTopComponent; - String nameOfNodeToSelect; - Node currentRootNodeOfResultsView; - - ResultViewNodeSelectionTask(DataResultTopComponent resultViewTopComponent, String nameToSelect, Node currentRootNodeOfResultsView) { - this.resultViewTopComponent = resultViewTopComponent; - this.nameOfNodeToSelect = nameToSelect; - this.currentRootNodeOfResultsView = currentRootNodeOfResultsView; - } + List lineage = new ArrayList<>(); @Override - protected Node[] doInBackground() throws Exception { - /* - * Return all of the child nodes of the current root node of the - * results view. Note that calls to Children.getNodes(true) block - * until all child nodes have been created, regardless of whether - * they are created lazily). There are two ideas here: 1) avoid - * getting a proxy "wait" node, and 2) do this in the background to - * avoid monopolizing the EDT for this potentially lengthy - * operation. - * - * RJCTODO: Is this all true? What if the user selects another node - * in the tree while this is going on? - */ - return currentRootNodeOfResultsView.getChildren().getNodes(true); - } - - @Override - protected void done() { - Node[] nodesDisplayedInResultsView; + protected List defaultVisit(Content content) { + lineage.add(content); + Content parent = null; try { - nodesDisplayedInResultsView = get(); - } catch (InterruptedException | CancellationException | ExecutionException ex) { - logger.log(Level.SEVERE, "ResultViewNodeSelectionTask failed to get nodes displayed in results view.", ex); //NON-NLS - return; - } - - // It is possible the user selected a different Node to be displayed - // in the DataResultViewer while the child Nodes were being generated. - // In that case, we don't want to set the selection because it the - // nodes returned from get() won't be in the DataResultTopComponent's - // ExplorerManager. If we did call setSelectedNodes, it would clear - // the current selection, which is not good. - if (resultViewTopComponent.getRootNode().equals(currentRootNodeOfResultsView) == false) { - return; - } - - // Find the correct node to select from the nodes that are displayed - // in the data result viewer and set it as the selection of the - // DataResultTopComponent. - for (Node node : nodesDisplayedInResultsView) { - if (nameOfNodeToSelect.equals(node.getName())) { - resultViewTopComponent.requestActive(); - resultViewTopComponent.setSelectedNodes(new Node[]{node}); - DirectoryTreeTopComponent.getDefault().fireViewerComplete(); - break; - } + parent = content.getParent(); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, String.format("Could not get parent of Content object: %s", content), ex); //NON-NLS } + return parent == null ? lineage : parent.accept(this); } - } - - /** - * A ReverseHierarchyVisitor is a ContentVisitor that returns a list of - * content objects by starting with a given content and following its chain - * of ancestors to the root content of the hierarchy. - */ - private class LeafToRootContentBranchVisitor extends ContentVisitor.Default> { - - List hierarchy = new ArrayList<>(); - @Override public List visit(VolumeSystem volumeSystem) { /* - * Volume systems are not shown in the tree view. + * Volume systems are not shown in the tree view. This is not + * strictly necesssary given the algorithm above, but it is a simple + * optimization. */ - return visitParentButDontAddMe(volumeSystem); + return skipToParent(volumeSystem); } @Override public List visit(FileSystem fileSystem) { /* - * File systems are not shown in the tree view. + * File systems are not shown in the tree view. This is not strictly + * necesssary given the algorithm above, but it is a simple + * optimization. */ - return visitParentButDontAddMe(fileSystem); + return skipToParent(fileSystem); } - @Override - public List visit(Directory directory) { - /* - * File system root directories are not shown in the tree view. - */ - if (ROOT_DIR_META_ADDR == directory.getMetaAddr()) { - return visitParentButDontAddMe(directory); - } else { - return defaultVisit(directory); - } - } - - @Override - protected List defaultVisit(Content content) { - hierarchy.add(content); + private List skipToParent(Content content) { Content parent = null; try { parent = content.getParent(); } catch (TskCoreException ex) { logger.log(Level.SEVERE, String.format("Could not get parent of Content object: %s", content), ex); //NON-NLS } - return parent == null ? hierarchy : parent.accept(this); + return parent == null ? lineage : parent.accept(this); } - - private List visitParentButDontAddMe(Content content) { - Content parent = null; - try { - parent = content.getParent(); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, String.format("Could not get parent of Content object: %s", content), ex); //NON-NLS - } - return parent == null ? hierarchy : parent.accept(this); - } - } + }