Autopsy  4.14.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
DirectoryTreeTopComponent.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2012-2020 Basis Technology Corp.
5  * Contact: carrier <at> sleuthkit <dot> org
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  * http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19 package org.sleuthkit.autopsy.directorytree;
20 
21 import java.awt.Cursor;
22 import java.awt.EventQueue;
23 import java.beans.PropertyChangeEvent;
24 import java.beans.PropertyChangeListener;
25 import java.beans.PropertyVetoException;
26 import java.io.IOException;
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.EnumSet;
30 import java.util.LinkedList;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.Objects;
34 import java.util.concurrent.ExecutionException;
35 import java.util.logging.Level;
36 import java.util.prefs.PreferenceChangeEvent;
37 import java.util.prefs.PreferenceChangeListener;
38 import javax.swing.Action;
39 import javax.swing.SwingUtilities;
40 import javax.swing.SwingWorker;
41 import javax.swing.event.PopupMenuEvent;
42 import javax.swing.event.PopupMenuListener;
43 import javax.swing.event.TreeExpansionEvent;
44 import javax.swing.event.TreeExpansionListener;
45 import javax.swing.tree.TreeSelectionModel;
46 import org.apache.commons.lang3.StringUtils;
47 import org.openide.explorer.ExplorerManager;
48 import org.openide.explorer.ExplorerUtils;
49 import org.openide.explorer.view.BeanTreeView;
50 import org.openide.explorer.view.Visualizer;
51 import org.openide.nodes.AbstractNode;
52 import org.openide.nodes.Children;
53 import org.openide.nodes.Node;
54 import org.openide.nodes.NodeNotFoundException;
55 import org.openide.nodes.NodeOp;
56 import org.openide.util.NbBundle;
57 import org.openide.util.NbBundle.Messages;
58 import org.openide.windows.TopComponent;
59 import org.openide.windows.WindowManager;
88 import org.sleuthkit.datamodel.Account;
89 import org.sleuthkit.datamodel.BlackboardArtifact;
90 import org.sleuthkit.datamodel.BlackboardAttribute;
91 import org.sleuthkit.datamodel.Content;
92 import org.sleuthkit.datamodel.TskCoreException;
93 
97 // Registered as a service provider for DataExplorer in layer.xml
98 @Messages({
99  "DirectoryTreeTopComponent.resultsView.title=Listing"
100 })
101 @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives
102 public final class DirectoryTreeTopComponent extends TopComponent implements DataExplorer, ExplorerManager.Provider {
103 
104  private final transient ExplorerManager em = new ExplorerManager();
106  private final DataResultTopComponent dataResult = new DataResultTopComponent(Bundle.DirectoryTreeTopComponent_resultsView_title());
107  private final ViewPreferencesPanel viewPreferencesPanel = new ViewPreferencesPanel(true);
108  private final LinkedList<String[]> backList;
109  private final LinkedList<String[]> forwardList;
110  private static final String PREFERRED_ID = "DirectoryTreeTopComponent"; //NON-NLS
111  private static final Logger LOGGER = Logger.getLogger(DirectoryTreeTopComponent.class.getName());
113  private Children autopsyTreeChildren;
115  private boolean showRejectedResults;
116  private static final long DEFAULT_DATASOURCE_GROUPING_THRESHOLD = 5; // Threshold for prompting the user about grouping by data source
117  private static final String GROUPING_THRESHOLD_NAME = "GroupDataSourceThreshold";
118  private static final String SETTINGS_FILE = "CasePreferences.properties"; //NON-NLS
119 
124  initComponents();
125 
126  // only allow one item to be selected at a time
127  getTree().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
128  //Hook into the JTree and pre-expand the Views Node and Results node when a user
129  //expands an item in the tree that makes these nodes visible.
130  ((ExpansionBeanTreeView) getTree()).addTreeExpansionListener(new TreeExpansionListener() {
131  @Override
132  public void treeExpanded(TreeExpansionEvent event) {
133  //Bail immediately if we are not in the Group By view.
134  //Assumption here is that the views are already expanded.
136  return;
137  }
138 
139  Node expandedNode = Visualizer.findNode(event.getPath().getLastPathComponent());
140  for (Node child : em.getRootContext().getChildren().getNodes()) {
141  if (child.equals(expandedNode)) {
142  preExpandNodes(child.getChildren());
143  }
144  }
145  }
146 
147  @Override
148  public void treeCollapsed(TreeExpansionEvent event) {
149  //Do nothing
150  }
151 
152  });
153  // remove the close button
154  putClientProperty(TopComponent.PROP_CLOSING_DISABLED, Boolean.TRUE);
155  setName(NbBundle.getMessage(DirectoryTreeTopComponent.class, "CTL_DirectoryTreeTopComponent"));
156  setToolTipText(NbBundle.getMessage(DirectoryTreeTopComponent.class, "HINT_DirectoryTreeTopComponent"));
157 
158  subscribeToChangeEvents();
159  associateLookup(ExplorerUtils.createLookup(em, getActionMap()));
160 
161  // set the back & forward list and also disable the back & forward button
162  this.backList = new LinkedList<>();
163  this.forwardList = new LinkedList<>();
164  backButton.setEnabled(false);
165  forwardButton.setEnabled(false);
166 
167  viewPreferencesPopupMenu.add(viewPreferencesPanel);
168  viewPreferencesPopupMenu.setSize(viewPreferencesPanel.getPreferredSize().width + 6, viewPreferencesPanel.getPreferredSize().height + 6);
169  viewPreferencesPopupMenu.addPopupMenuListener(new PopupMenuListener() {
170  @Override
171  public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
172  openViewPreferencesButton.setSelected(true);
173  }
174 
175  @Override
176  public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
177  openViewPreferencesButton.setSelected(false);
178  }
179 
180  @Override
181  public void popupMenuCanceled(PopupMenuEvent e) {
182  openViewPreferencesButton.setSelected(false);
183  }
184  });
185  }
186 
193  private void preExpandNodes(Children rootChildren) {
194  BeanTreeView tree = getTree();
195 
196  Node results = rootChildren.findChild(ResultsNode.NAME);
197  if (!Objects.isNull(results)) {
198  tree.expandNode(results);
199  Children resultsChildren = results.getChildren();
200  Arrays.stream(resultsChildren.getNodes()).forEach(tree::expandNode);
201  }
202 
203  Node views = rootChildren.findChild(ViewsNode.NAME);
204  if (!Objects.isNull(views)) {
205  tree.expandNode(views);
206  }
207  }
208 
212  private void subscribeToChangeEvents() {
213  UserPreferences.addChangeListener(new PreferenceChangeListener() {
214  @Override
215  public void preferenceChange(PreferenceChangeEvent evt) {
216  switch (evt.getKey()) {
225  refreshContentTreeSafe();
226  break;
228  refreshTagsTree();
229  break;
232  // TODO: Need a way to refresh the Views subtree alone.
233  refreshContentTreeSafe();
234  break;
235  }
236  }
237  });
238 
240  this.em.addPropertyChangeListener(this);
241  }
242 
244  this.dataResult.requestActive();
245  }
246 
247  public void openDirectoryListing() {
248  this.dataResult.open();
249  }
250 
252  return this.dataResult;
253  }
254 
260  public boolean getShowRejectedResults() {
261  return showRejectedResults;
262  }
263 
270  public void setShowRejectedResults(boolean showRejectedResults) {
271  this.showRejectedResults = showRejectedResults;
272  if (accounts != null) {
273  accounts.setShowRejected(showRejectedResults);
274  }
275  }
276 
282  // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
283  private void initComponents() {
284 
285  viewPreferencesPopupMenu = new javax.swing.JPopupMenu();
286  treeView = new ExpansionBeanTreeView();
287  backButton = new javax.swing.JButton();
288  forwardButton = new javax.swing.JButton();
289  openViewPreferencesButton = new javax.swing.JButton();
290 
291  treeView.setBorder(null);
292 
293  backButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/directorytree/btn_step_back.png"))); // NOI18N
294  org.openide.awt.Mnemonics.setLocalizedText(backButton, org.openide.util.NbBundle.getMessage(DirectoryTreeTopComponent.class, "DirectoryTreeTopComponent.backButton.text")); // NOI18N
295  backButton.setBorderPainted(false);
296  backButton.setContentAreaFilled(false);
297  backButton.setDisabledIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/directorytree/btn_step_back_disabled.png"))); // NOI18N
298  backButton.setMargin(new java.awt.Insets(2, 0, 2, 0));
299  backButton.setMaximumSize(new java.awt.Dimension(55, 100));
300  backButton.setMinimumSize(new java.awt.Dimension(5, 5));
301  backButton.setPreferredSize(new java.awt.Dimension(24, 24));
302  backButton.setRolloverIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/directorytree/btn_step_back_hover.png"))); // NOI18N
303  backButton.addActionListener(new java.awt.event.ActionListener() {
304  public void actionPerformed(java.awt.event.ActionEvent evt) {
305  backButtonActionPerformed(evt);
306  }
307  });
308 
309  forwardButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/directorytree/btn_step_forward.png"))); // NOI18N
310  org.openide.awt.Mnemonics.setLocalizedText(forwardButton, org.openide.util.NbBundle.getMessage(DirectoryTreeTopComponent.class, "DirectoryTreeTopComponent.forwardButton.text")); // NOI18N
311  forwardButton.setBorderPainted(false);
312  forwardButton.setContentAreaFilled(false);
313  forwardButton.setDisabledIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/directorytree/btn_step_forward_disabled.png"))); // NOI18N
314  forwardButton.setMargin(new java.awt.Insets(2, 0, 2, 0));
315  forwardButton.setMaximumSize(new java.awt.Dimension(55, 100));
316  forwardButton.setMinimumSize(new java.awt.Dimension(5, 5));
317  forwardButton.setPreferredSize(new java.awt.Dimension(24, 24));
318  forwardButton.setRolloverIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/directorytree/btn_step_forward_hover.png"))); // NOI18N
319  forwardButton.addActionListener(new java.awt.event.ActionListener() {
320  public void actionPerformed(java.awt.event.ActionEvent evt) {
321  forwardButtonActionPerformed(evt);
322  }
323  });
324 
325  openViewPreferencesButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/directorytree/view-preferences-23.png"))); // NOI18N
326  org.openide.awt.Mnemonics.setLocalizedText(openViewPreferencesButton, org.openide.util.NbBundle.getMessage(DirectoryTreeTopComponent.class, "DirectoryTreeTopComponent.openViewPreferencesButton.text")); // NOI18N
327  openViewPreferencesButton.setBorder(javax.swing.BorderFactory.createEmptyBorder(1, 1, 1, 1));
328  openViewPreferencesButton.setBorderPainted(false);
329  openViewPreferencesButton.setContentAreaFilled(false);
330  openViewPreferencesButton.setMaximumSize(new java.awt.Dimension(24, 24));
331  openViewPreferencesButton.setMinimumSize(new java.awt.Dimension(24, 24));
332  openViewPreferencesButton.setPreferredSize(new java.awt.Dimension(24, 24));
333  openViewPreferencesButton.addActionListener(new java.awt.event.ActionListener() {
334  public void actionPerformed(java.awt.event.ActionEvent evt) {
335  openViewPreferencesButtonActionPerformed(evt);
336  }
337  });
338 
339  javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
340  this.setLayout(layout);
341  layout.setHorizontalGroup(
342  layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
343  .addComponent(treeView)
344  .addGroup(layout.createSequentialGroup()
345  .addContainerGap()
346  .addComponent(backButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
347  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
348  .addComponent(forwardButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
349  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 140, Short.MAX_VALUE)
350  .addComponent(openViewPreferencesButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
351  .addContainerGap())
352  );
353  layout.setVerticalGroup(
354  layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
355  .addGroup(layout.createSequentialGroup()
356  .addGap(0, 0, 0)
357  .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
358  .addComponent(openViewPreferencesButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
359  .addComponent(backButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
360  .addComponent(forwardButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
361  .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
362  .addComponent(treeView, javax.swing.GroupLayout.DEFAULT_SIZE, 919, Short.MAX_VALUE))
363  );
364  }// </editor-fold>//GEN-END:initComponents
365 
366  private void backButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_backButtonActionPerformed
367  // change the cursor to "waiting cursor" for this operation
368  this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
369 
370  // the end is the current place,
371  String[] currentNodePath = backList.pollLast();
372  forwardList.addLast(currentNodePath);
373  forwardButton.setEnabled(true);
374 
375  /*
376  * We peek instead of poll because we use its existence in the list
377  * later on so that we do not reset the forward list after the selection
378  * occurs.
379  */
380  String[] newCurrentNodePath = backList.peekLast();
381 
382  // enable / disable the back and forward button
383  if (backList.size() > 1) {
384  backButton.setEnabled(true);
385  } else {
386  backButton.setEnabled(false);
387  }
388 
389  // update the selection on directory tree
390  setSelectedNode(newCurrentNodePath, null);
391 
392  this.setCursor(null);
393  }//GEN-LAST:event_backButtonActionPerformed
394 
395  private void forwardButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_forwardButtonActionPerformed
396  // change the cursor to "waiting cursor" for this operation
397  this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
398 
399  String[] newCurrentNodePath = forwardList.pollLast();
400  if (!forwardList.isEmpty()) {
401  forwardButton.setEnabled(true);
402  } else {
403  forwardButton.setEnabled(false);
404  }
405 
406  backList.addLast(newCurrentNodePath);
407  backButton.setEnabled(true);
408 
409  // update the selection on directory tree
410  setSelectedNode(newCurrentNodePath, null);
411 
412  this.setCursor(null);
413  }//GEN-LAST:event_forwardButtonActionPerformed
414 
415  private void openViewPreferencesButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_openViewPreferencesButtonActionPerformed
416  viewPreferencesPanel.load();
417  viewPreferencesPopupMenu.show(openViewPreferencesButton, 0, openViewPreferencesButton.getHeight() - 1);
418  }//GEN-LAST:event_openViewPreferencesButtonActionPerformed
419 
420  // Variables declaration - do not modify//GEN-BEGIN:variables
421  private javax.swing.JButton backButton;
422  private javax.swing.JButton forwardButton;
423  private javax.swing.JButton openViewPreferencesButton;
424  private javax.swing.JScrollPane treeView;
425  private javax.swing.JPopupMenu viewPreferencesPopupMenu;
426  // End of variables declaration//GEN-END:variables
427 
436  public static synchronized DirectoryTreeTopComponent getDefault() {
437  if (instance == null) {
438  instance = new DirectoryTreeTopComponent();
439  }
440  return instance;
441  }
442 
449  public static synchronized DirectoryTreeTopComponent findInstance() {
450  WindowManager winManager = WindowManager.getDefault();
451  TopComponent win = winManager.findTopComponent(PREFERRED_ID);
452  if (win == null) {
453  LOGGER.warning(
454  "Cannot find " + PREFERRED_ID + " component. It will not be located properly in the window system."); //NON-NLS
455  return getDefault();
456  }
457  if (win instanceof DirectoryTreeTopComponent) {
458  return (DirectoryTreeTopComponent) win;
459  }
460  LOGGER.warning(
461  "There seem to be multiple components with the '" + PREFERRED_ID //NON-NLS
462  + "' ID. That is a potential source of errors and unexpected behavior."); //NON-NLS
463  return getDefault();
464  }
465 
472  @Override
473  public int getPersistenceType() {
474  return TopComponent.PERSISTENCE_NEVER;
475  }
476 
483  private void promptForDataSourceGrouping(int dataSourceCount) {
485  GroupDataSourcesDialog dialog = new GroupDataSourcesDialog(dataSourceCount);
486  dialog.display();
487  if (dialog.groupByDataSourceSelected()) {
489  refreshContentTreeSafe();
490  } else {
492  }
493  }
494  }
495 
503  @NbBundle.Messages({"# {0} - dataSourceCount",
504  "DirectoryTreeTopComponent.componentOpened.groupDataSources.text=This case contains {0} data sources. Would you like to group by data source for faster loading?",
505  "DirectoryTreeTopComponent.componentOpened.groupDataSources.title=Group by data source?"})
506  @Override
507  public void componentOpened() {
508  // change the cursor to "waiting cursor" for this operation
509  this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
510  Case currentCase = null;
511  try {
512  currentCase = Case.getCurrentCaseThrows();
513  } catch (NoCurrentCaseException ex) {
514  // No open case.
515  }
516 
517  // close the top component if there's no image in this case
518  if (null == currentCase || currentCase.hasData() == false) {
519  getTree().setRootVisible(false); // hide the root
520  } else {
521  // If the case contains a lot of data sources, and they aren't already grouping
522  // by data source, give the user the option to do so before loading the tree.
524  long threshold = DEFAULT_DATASOURCE_GROUPING_THRESHOLD;
525  if (ModuleSettings.settingExists(ModuleSettings.MAIN_SETTINGS, GROUPING_THRESHOLD_NAME)) {
526  try {
527  threshold = Long.parseLong(ModuleSettings.getConfigSetting(ModuleSettings.MAIN_SETTINGS, GROUPING_THRESHOLD_NAME));
528  } catch (NumberFormatException ex) {
529  LOGGER.log(Level.SEVERE, "Group data sources threshold is not a number", ex);
530  }
531  } else {
532  ModuleSettings.setConfigSetting(ModuleSettings.MAIN_SETTINGS, GROUPING_THRESHOLD_NAME, String.valueOf(threshold));
533  }
534 
535  try {
536  int dataSourceCount = currentCase.getDataSources().size();
537  if (!Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true)
538  && dataSourceCount > threshold) {
539  promptForDataSourceGrouping(dataSourceCount);
540  }
541  } catch (TskCoreException ex) {
542  LOGGER.log(Level.SEVERE, "Error loading data sources", ex);
543  }
544  }
545 
546  // if there's at least one image, load the image and open the top componen
547  autopsyTreeChildFactory = new AutopsyTreeChildFactory();
548  autopsyTreeChildren = Children.create(autopsyTreeChildFactory, true);
549  Node root = new AbstractNode(autopsyTreeChildren) {
550  //JIRA-2807: What is the point of these overrides?
555  @Override
556  public Action[] getActions(boolean popup) {
557  return new Action[]{};
558  }
559 
560  // Overide the AbstractNode use of DefaultHandle to return
561  // a handle which can be serialized without a parent
562  @Override
563  public Node.Handle getHandle() {
564  return new Node.Handle() {
565  @Override
566  public Node getNode() throws IOException {
567  return em.getRootContext();
568  }
569  };
570  }
571  };
572 
573  root = new DirectoryTreeFilterNode(root, true);
574 
575  em.setRootContext(root);
576  em.getRootContext().setName(currentCase.getName());
577  em.getRootContext().setDisplayName(currentCase.getName());
578  getTree().setRootVisible(false); // hide the root
579 
580  // Reset the forward and back lists because we're resetting the root context
581  resetHistory();
582  new SwingWorker<Node[], Void>() {
583  @Override
584  protected Node[] doInBackground() throws Exception {
585  Children rootChildren = em.getRootContext().getChildren();
586  preExpandNodes(rootChildren);
587  /*
588  * JIRA-2806: What is this supposed to do? Right now it
589  * selects the data sources node, but the comment seems to
590  * indicate it is supposed to select the first datasource.
591  */
592  // select the first image node, if there is one
593  // (this has to happen after dataResult is opened, because the event
594  // of changing the selected node fires a handler that tries to make
595  // dataResult active)
596  if (rootChildren.getNodesCount() > 0) {
597  return new Node[]{rootChildren.getNodeAt(0)};
598  }
599  return new Node[]{};
600  }
601 
602  @Override
603  protected void done() {
604  super.done();
605 
606  // if the dataResult is not opened
607  if (!dataResult.isOpened()) {
608  dataResult.open(); // open the data result top component as well when the directory tree is opened
609  }
610  /*
611  * JIRA-2806: What is this supposed to do?
612  */
613  // select the first image node, if there is one
614  // (this has to happen after dataResult is opened, because the event
615  // of changing the selected node fires a handler that tries to make
616  // dataResult active)
617  try {
618  Node[] selections = get();
619  if (selections != null && selections.length > 0) {
620  em.setSelectedNodes(selections);
621  }
622  } catch (PropertyVetoException ex) {
623  LOGGER.log(Level.SEVERE, "Error setting default selected node.", ex); //NON-NLS
624  } catch (InterruptedException | ExecutionException ex) {
625  LOGGER.log(Level.SEVERE, "Error expanding tree to initial state.", ex); //NON-NLS
626  } finally {
627  setCursor(null);
628  }
629  }
630  }.execute();
631  }
632  }
633 
640  @Override
641  public void componentClosed() {
642  //@@@ push the selection node to null?
643  autopsyTreeChildren = null;
644  }
645 
646  void writeProperties(java.util.Properties p) {
647  // better to version settings since initial version as advocated at
648  // http://wiki.apidesign.org/wiki/PropertyFiles
649  p.setProperty("version", "1.0");
650  // TODO store your settings
651  }
652 
653  Object readProperties(java.util.Properties p) {
654  if (instance == null) {
655  instance = this;
656  }
657  instance.readPropertiesImpl(p);
658  return instance;
659  }
660 
661  private void readPropertiesImpl(java.util.Properties p) {
662  String version = p.getProperty("version");
663  // TODO read your settings according to their version
664  }
665 
671  @Override
672  protected String preferredID() {
673  return PREFERRED_ID;
674  }
675 
676  @Override
677  public boolean canClose() {
678  /*
679  * Only allow the main tree view in the left side of the main window to
680  * be closed if there is no opne case or the open case has no data
681  * sources.
682  */
683  try {
684  Case openCase = Case.getCurrentCaseThrows();
685  return openCase.hasData() == false;
686  } catch (NoCurrentCaseException ex) {
687  return true;
688  }
689  }
690 
696  @Override
697  public ExplorerManager getExplorerManager() {
698  return this.em;
699  }
700 
706  @Override
707  public Action[] getActions() {
708  return new Action[]{};
709  }
710 
716  public Node getSelectedNode() {
717  Node result = null;
718 
719  Node[] selectedNodes = this.getExplorerManager().getSelectedNodes();
720  if (selectedNodes.length > 0) {
721  result = selectedNodes[0];
722  }
723  return result;
724  }
725 
732  @Override
733  public void propertyChange(PropertyChangeEvent event) {
735  String changed = event.getPropertyName();
736  if (changed.equals(Case.Events.CURRENT_CASE.toString())) { // changed current case
737  // When a case is closed, the old value of this property is the
738  // closed Case object and the new value is null. When a case is
739  // opened, the old value is null and the new value is the new Case
740  // object.
741  // @@@ This needs to be revisited. Perhaps case closed and case
742  // opened events instead of property change events would be a better
743  // solution. Either way, more probably needs to be done to clean up
744  // data model objects when a case is closed.
745  if (event.getOldValue() != null && event.getNewValue() == null) {
746  // The current case has been closed. Reset the ExplorerManager.
747  SwingUtilities.invokeLater(() -> {
748  Node emptyNode = new AbstractNode(Children.LEAF);
749  em.setRootContext(emptyNode);
750  });
751  } else if (event.getNewValue() != null) {
752  // A new case has been opened. Reset the ExplorerManager.
753  Case newCase = (Case) event.getNewValue();
754  final String newCaseName = newCase.getName();
755  SwingUtilities.invokeLater(() -> {
756  em.getRootContext().setName(newCaseName);
757  em.getRootContext().setDisplayName(newCaseName);
758 
759  // Reset the forward and back
760  // buttons. Note that a call to CoreComponentControl.openCoreWindows()
761  // by the new Case object will lead to a componentOpened() call
762  // that will repopulate the tree.
763  // @@@ The repopulation of the tree in this fashion also merits
764  // reconsideration.
765  resetHistory();
766  });
767  }
768  } // if the image is added to the case
769  else if (changed.equals(Case.Events.DATA_SOURCE_ADDED.toString())) {
776  try {
778  /*
779  * In case the Case 'updateGUIForCaseOpened()' method hasn't
780  * already done so, open the tree and all other core
781  * windows.
782  *
783  * TODO: (JIRA-4053) DirectoryTreeTopComponent should not be
784  * responsible for opening core windows. Consider moving
785  * this elsewhere.
786  */
787  SwingUtilities.invokeLater(() -> {
788  if (!DirectoryTreeTopComponent.this.isOpened()) {
790  }
791  });
792  } catch (NoCurrentCaseException notUsed) {
796  }
797  } // change in node selection
798  else if (changed.equals(ExplorerManager.PROP_SELECTED_NODES)) {
799  respondSelection((Node[]) event.getOldValue(), (Node[]) event.getNewValue());
800  }
801  }
802  }
803 
812  @NbBundle.Messages("DirectoryTreeTopComponent.emptyMimeNode.text=Data not available. Run file type identification module.")
813  void respondSelection(final Node[] oldNodes, final Node[] newNodes) {
814  if (!Case.isCaseOpen()) {
815  return;
816  }
817 
818  // Some lock that prevents certain Node operations is set during the
819  // ExplorerManager selection-change, so we must handle changes after the
820  // selection-change event is processed.
821  //TODO find a different way to refresh data result viewer, scheduling this
822  //to EDT breaks loading of nodes in the background
823  EventQueue.invokeLater(() -> {
824  // change the cursor to "waiting cursor" for this operation
825  DirectoryTreeTopComponent.this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
826  try {
827  Node treeNode = DirectoryTreeTopComponent.this.getSelectedNode();
828  if (treeNode != null) {
829  Node originNode = ((DirectoryTreeFilterNode) treeNode).getOriginal();
830  //set node, wrap in filter node first to filter out children
831  Node drfn = new DataResultFilterNode(originNode, DirectoryTreeTopComponent.this.em);
832  // Create a TableFilterNode with knowledge of the node's type to allow for column order settings
833  if (FileTypesByMimeType.isEmptyMimeTypeNode(originNode)) {
834  //Special case for when File Type Identification has not yet been run and
835  //there are no mime types to populate Files by Mime Type Tree
836  EmptyNode emptyNode = new EmptyNode(Bundle.DirectoryTreeTopComponent_emptyMimeNode_text());
837  dataResult.setNode(new TableFilterNode(emptyNode, true, "This Node Is Empty")); //NON-NLS
838  } else if (originNode instanceof DisplayableItemNode) {
839  dataResult.setNode(new TableFilterNode(drfn, true, ((DisplayableItemNode) originNode).getItemType()));
840  } else {
841  dataResult.setNode(new TableFilterNode(drfn, true));
842  }
843  String displayName = "";
844  Content content = originNode.getLookup().lookup(Content.class);
845  if (content != null) {
846  try {
847  displayName = content.getUniquePath();
848  } catch (TskCoreException ex) {
849  LOGGER.log(Level.SEVERE, "Exception while calling Content.getUniquePath() for node: {0}", originNode); //NON-NLS
850  }
851  } else if (originNode.getLookup().lookup(String.class) != null) {
852  displayName = originNode.getLookup().lookup(String.class);
853  }
854  dataResult.setPath(displayName);
855  }
856  // set the directory listing to be active
857  if (oldNodes != null && newNodes != null
858  && (oldNodes.length == newNodes.length)) {
859  boolean sameNodes = true;
860  for (int i = 0; i < oldNodes.length; i++) {
861  sameNodes = sameNodes && oldNodes[i].getName().equals(newNodes[i].getName());
862  }
863  if (!sameNodes) {
864  dataResult.requestActive();
865  }
866  }
867  } finally {
868  setCursor(null);
869  }
870  });
871 
872  // update the back and forward list
873  updateHistory(em.getSelectedNodes());
874  }
875 
876  private void updateHistory(Node[] selectedNodes) {
877  if (selectedNodes.length == 0) {
878  return;
879  }
880 
881  Node selectedNode = selectedNodes[0];
882  String selectedNodeName = selectedNode.getName();
883 
884  /*
885  * get the previous entry to make sure we don't duplicate it. Motivation
886  * for this is also that if we used the back button, then we already
887  * added the 'current' node to 'back' and we will detect that and not
888  * reset the forward list.
889  */
890  String[] currentLast = backList.peekLast();
891  String lastNodeName = null;
892  if (currentLast != null && currentLast.length > 0) {
893  lastNodeName = currentLast[currentLast.length - 1];
894  }
895 
896  if (currentLast == null || !selectedNodeName.equals(lastNodeName)) {
897  //add to the list if the last if not the same as current
898  final String[] selectedPath = NodeOp.createPath(selectedNode, em.getRootContext());
899  backList.addLast(selectedPath); // add the node to the "backList"
900  if (backList.size() > 1) {
901  backButton.setEnabled(true);
902  } else {
903  backButton.setEnabled(false);
904  }
905 
906  forwardList.clear(); // clear the "forwardList"
907  forwardButton.setEnabled(false); // disable the forward Button
908  }
909  }
910 
915  private void resetHistory() {
916  // clear the back and forward list
917  backList.clear();
918  forwardList.clear();
919  backButton.setEnabled(false);
920  forwardButton.setEnabled(false);
921  }
922 
928  BeanTreeView getTree() {
929  return (BeanTreeView) this.treeView;
930  }
931 
935  public void refreshContentTreeSafe() {
936  SwingUtilities.invokeLater(this::rebuildTree);
937  }
938 
942  private void refreshTagsTree() {
943  SwingUtilities.invokeLater(() -> {
944  // Ensure the component children have been created first.
945  if (autopsyTreeChildren == null) {
946  return;
947  }
948 
949  if (Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true)) {
950  for (Node dataSource : autopsyTreeChildren.getNodes()) {
951  Node tagsNode = dataSource.getChildren().findChild(Tags.getTagsDisplayName());
952  if (tagsNode != null) {
953  //Reports is at the same level as the data sources so we want to ignore it
954  ((Tags.RootNode) tagsNode).refresh();
955  }
956  }
957  } else {
958  Node tagsNode = autopsyTreeChildren.findChild(Tags.getTagsDisplayName());
959  if (tagsNode != null) {
960  ((Tags.RootNode) tagsNode).refresh();
961  }
962  }
963  });
964  }
965 
971  private void rebuildTree() {
972 
973  // if no open case or has no data then there is no tree to rebuild
974  Case currentCase;
975  try {
976  currentCase = Case.getCurrentCaseThrows();
977  } catch (NoCurrentCaseException ex) {
978  return;
979  }
980  if (null == currentCase || currentCase.hasData() == false) {
981  return;
982  }
983 
984  // refresh all children of the root.
985  autopsyTreeChildFactory.refreshChildren();
986 
987  // Select the first node and reset the selection history
988  // This should happen on the EDT once the tree has been rebuilt.
989  // hence the SwingWorker that does this in the done() method
990  new SwingWorker<Void, Void>() {
991 
992  @Override
993  protected Void doInBackground() throws Exception {
994  return null;
995  }
996 
997  @Override
998  protected void done() {
999  super.done();
1000  try {
1001  get();
1002  resetHistory();
1003  preExpandNodes(em.getRootContext().getChildren());
1004  } catch (InterruptedException | ExecutionException ex) {
1005  LOGGER.log(Level.SEVERE, "Error selecting tree node.", ex); //NON-NLS
1006  } //NON-NLS
1007  }
1008  }.execute();
1009  }
1010 
1018  private void setSelectedNode(final String[] previouslySelectedNodePath, final String rootNodeName) {
1019  if (previouslySelectedNodePath == null) {
1020  return;
1021  }
1022  SwingUtilities.invokeLater(new Runnable() {
1023  @Override
1024  public void run() {
1025  if (previouslySelectedNodePath.length > 0 && (rootNodeName == null || previouslySelectedNodePath[0].equals(rootNodeName))) {
1026  Node selectedNode = null;
1027  ArrayList<String> selectedNodePath = new ArrayList<>(Arrays.asList(previouslySelectedNodePath));
1028  while (null == selectedNode && !selectedNodePath.isEmpty()) {
1029  try {
1030  selectedNode = NodeOp.findPath(em.getRootContext(), selectedNodePath.toArray(new String[selectedNodePath.size()]));
1031  } catch (NodeNotFoundException ex) {
1032  // The selected node may have been deleted (e.g., a deleted tag), so truncate the path and try again.
1033  if (selectedNodePath.size() > 1) {
1034  selectedNodePath.remove(selectedNodePath.size() - 1);
1035  } else {
1036  StringBuilder nodePath = new StringBuilder();
1037  for (int i = 0; i < previouslySelectedNodePath.length; ++i) {
1038  nodePath.append(previouslySelectedNodePath[i]).append("/");
1039  }
1040  LOGGER.log(Level.WARNING, "Failed to find any nodes to select on path " + nodePath.toString(), ex); //NON-NLS
1041  break;
1042  }
1043  }
1044  }
1045 
1046  if (null != selectedNode) {
1047  if (rootNodeName != null) {
1048  //called from tree auto refresh context
1049  //remove last from backlist, because auto select will result in duplication
1050  backList.pollLast();
1051  }
1052  try {
1053  em.setExploredContextAndSelection(selectedNode, new Node[]{selectedNode});
1054  } catch (PropertyVetoException ex) {
1055  LOGGER.log(Level.WARNING, "Property veto from ExplorerManager setting selection to " + selectedNode.getName(), ex); //NON-NLS
1056  }
1057  }
1058  }
1059  }
1060  });
1061  }
1062 
1063  @Override
1064  public TopComponent getTopComponent() {
1065  return this;
1066  }
1067 
1068  @Override
1069  public boolean hasMenuOpenAction() {
1070  return false;
1071  }
1072 
1073  public void viewArtifact(final BlackboardArtifact art) {
1074  int typeID = art.getArtifactTypeID();
1075  String typeName = art.getArtifactTypeName();
1076  Children rootChilds = em.getRootContext().getChildren();
1077  Node treeNode = null;
1078  Node resultsNode = rootChilds.findChild(ResultsNode.NAME);
1079  Children resultsChilds = resultsNode.getChildren();
1080  if (typeID == BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID()) {
1081  Node hashsetRootNode = resultsChilds.findChild(typeName);
1082  Children hashsetRootChilds = hashsetRootNode.getChildren();
1083  try {
1084  String setName = null;
1085  List<BlackboardAttribute> attributes = art.getAttributes();
1086  for (BlackboardAttribute att : attributes) {
1087  int typeId = att.getAttributeType().getTypeID();
1088  if (typeId == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID()) {
1089  setName = att.getValueString();
1090  }
1091  }
1092  treeNode = hashsetRootChilds.findChild(setName);
1093  } catch (TskCoreException ex) {
1094  LOGGER.log(Level.WARNING, "Error retrieving attributes", ex); //NON-NLS
1095  }
1096  } else if (typeID == BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID()) {
1097  Node keywordRootNode = resultsChilds.findChild(typeName);
1098  Children keywordRootChilds = keywordRootNode.getChildren();
1099  try {
1100  String listName = null;
1101  String keywordName = null;
1102  String regex = null;
1103  List<BlackboardAttribute> attributes = art.getAttributes();
1104  for (BlackboardAttribute att : attributes) {
1105  int typeId = att.getAttributeType().getTypeID();
1106  if (typeId == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID()) {
1107  listName = att.getValueString();
1108  } else if (typeId == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD.getTypeID()) {
1109  keywordName = att.getValueString();
1110  } else if (typeId == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD_REGEXP.getTypeID()) {
1111  regex = att.getValueString();
1112  }
1113  }
1114  if (listName == null) {
1115  if (regex == null) { //using same labels used for creation
1116  listName = NbBundle.getMessage(KeywordHits.class, "KeywordHits.simpleLiteralSearch.text");
1117  } else {
1118  listName = NbBundle.getMessage(KeywordHits.class, "KeywordHits.singleRegexSearch.text");
1119  }
1120  }
1121  Node listNode = keywordRootChilds.findChild(listName);
1122  if (listNode == null) {
1123  return;
1124  }
1125  Children listChildren = listNode.getChildren();
1126  if (listChildren == null) {
1127  return;
1128  }
1129  if (regex != null) { //For support of regex nodes such as URLs, IPs, Phone Numbers, and Email Addrs as they are down another level
1130  Node regexNode = listChildren.findChild(regex);
1131  if (regexNode == null) {
1132  return;
1133  }
1134  listChildren = regexNode.getChildren();
1135  if (listChildren == null) {
1136  return;
1137  }
1138  }
1139 
1140  treeNode = listChildren.findChild(keywordName);
1141 
1142  } catch (TskCoreException ex) {
1143  LOGGER.log(Level.WARNING, "Error retrieving attributes", ex); //NON-NLS
1144  }
1145  } else if (typeID == BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getTypeID()
1146  || typeID == BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_ARTIFACT_HIT.getTypeID()) {
1147  Node interestingItemsRootNode = resultsChilds.findChild(NbBundle
1148  .getMessage(InterestingHits.class, "InterestingHits.interestingItems.text"));
1149  Children interestingItemsRootChildren = interestingItemsRootNode.getChildren();
1150  try {
1151  String setName = null;
1152  List<BlackboardAttribute> attributes = art.getAttributes();
1153  for (BlackboardAttribute att : attributes) {
1154  int typeId = att.getAttributeType().getTypeID();
1155  if (typeId == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID()) {
1156  setName = att.getValueString();
1157  }
1158  }
1159  Node setNode = interestingItemsRootChildren.findChild(setName);
1160  if (setNode == null) {
1161  return;
1162  }
1163  Children interestingChildren = setNode.getChildren();
1164  if (interestingChildren == null) {
1165  return;
1166  }
1167  treeNode = interestingChildren.findChild(art.getDisplayName());
1168  } catch (TskCoreException ex) {
1169  LOGGER.log(Level.WARNING, "Error retrieving attributes", ex); //NON-NLS
1170  }
1171  } else if (typeID == BlackboardArtifact.ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID()) {
1172  Node emailMsgRootNode = resultsChilds.findChild(typeName);
1173  Children emailMsgRootChilds = emailMsgRootNode.getChildren();
1174  Map<String, String> parsedPath = null;
1175  try {
1176  List<BlackboardAttribute> attributes = art.getAttributes();
1177  for (BlackboardAttribute att : attributes) {
1178  int typeId = att.getAttributeType().getTypeID();
1179  if (typeId == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH.getTypeID()) {
1180  parsedPath = EmailExtracted.parsePath(att.getValueString());
1181  break;
1182  }
1183  }
1184  if (parsedPath == null) {
1185  return;
1186  }
1187  Node defaultNode = emailMsgRootChilds.findChild(parsedPath.get(NbBundle.getMessage(EmailExtracted.class, "EmailExtracted.defaultAcct.text")));
1188  Children defaultChildren = defaultNode.getChildren();
1189  treeNode = defaultChildren.findChild(parsedPath.get(NbBundle.getMessage(EmailExtracted.class, "EmailExtracted.defaultFolder.text")));
1190  } catch (TskCoreException ex) {
1191  LOGGER.log(Level.WARNING, "Error retrieving attributes", ex); //NON-NLS
1192  }
1193 
1194  } else if (typeID == BlackboardArtifact.ARTIFACT_TYPE.TSK_ACCOUNT.getTypeID()) {
1195  Node accountRootNode = resultsChilds.findChild(art.getDisplayName());
1196  Children accountRootChilds = accountRootNode.getChildren();
1197  List<BlackboardAttribute> attributes;
1198  String accountType = null;
1199  String ccNumberName = null;
1200  try {
1201  attributes = art.getAttributes();
1202  for (BlackboardAttribute att : attributes) {
1203  int typeId = att.getAttributeType().getTypeID();
1204  if (typeId == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ACCOUNT_TYPE.getTypeID()) {
1205  accountType = att.getValueString();
1206  }
1207  if (typeId == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_CARD_NUMBER.getTypeID()) {
1208  ccNumberName = att.getValueString();
1209  }
1210  }
1211  if (accountType == null) {
1212  return;
1213  }
1214 
1215  if (accountType.equals(Account.Type.CREDIT_CARD.getTypeName())) {
1216  Node accountNode = accountRootChilds.findChild(Account.Type.CREDIT_CARD.getDisplayName());
1217  if (accountNode == null) {
1218  return;
1219  }
1220  Children accountChildren = accountNode.getChildren();
1221  if (accountChildren == null) {
1222  return;
1223  }
1224  Node binNode = accountChildren.findChild(NbBundle.getMessage(Accounts.class, "Accounts.ByBINNode.name"));
1225  if (binNode == null) {
1226  return;
1227  }
1228  Children binChildren = binNode.getChildren();
1229  if (ccNumberName == null) {
1230  return;
1231  }
1232  //right padded with 0s to 8 digits when single number
1233  //when a range of numbers, the first 6 digits are rightpadded with 0s to 8 digits then a dash then 3 digits, the 6,7,8, digits of the end number right padded with 9s
1234  String binName = StringUtils.rightPad(ccNumberName, 8, "0");
1235  binName = binName.substring(0, 8);
1236  int bin;
1237  try {
1238  bin = Integer.parseInt(binName);
1239  } catch (NumberFormatException ex) {
1240  LOGGER.log(Level.WARNING, "Unable to parseInt a BIN for node selection from string binName=" + binName, ex); //NON-NLS
1241  return;
1242  }
1244  if (binInfo != null) {
1245  int startBin = ((BINRange) binInfo).getBINstart();
1246  int endBin = ((BINRange) binInfo).getBINend();
1247  if (startBin != endBin) {
1248  binName = Integer.toString(startBin) + "-" + Integer.toString(endBin).substring(5); //if there is a range re-construct the name it appears as
1249  }
1250  }
1251  if (binName == null) {
1252  return;
1253  }
1254  treeNode = binChildren.findChild(binName);
1255  } else { //default account type
1256  treeNode = accountRootChilds.findChild(accountType);
1257  }
1258  } catch (TskCoreException ex) {
1259  LOGGER.log(Level.WARNING, "Error retrieving attributes", ex); //NON-NLS
1260  }
1261  } else {
1262  Node extractedContent = resultsChilds.findChild(ExtractedContent.NAME);
1263  Children extractedChilds = extractedContent.getChildren();
1264  if (extractedChilds == null) {
1265  return;
1266  }
1267  treeNode = extractedChilds.findChild(typeName);
1268  }
1269 
1270  if (treeNode == null) {
1271  return;
1272  }
1273 
1274  DisplayableItemNode undecoratedParentNode = (DisplayableItemNode) ((DirectoryTreeFilterNode) treeNode).getOriginal();
1275  undecoratedParentNode.setChildNodeSelectionInfo(new ArtifactNodeSelectionInfo(art));
1276  getTree().expandNode(treeNode);
1277  if (this.getSelectedNode().equals(treeNode)) {
1278  this.setDirectoryListingActive();
1279  this.respondSelection(em.getSelectedNodes(), new Node[]{treeNode});
1280  } else {
1281  try {
1282  em.setExploredContextAndSelection(treeNode, new Node[]{treeNode});
1283  } catch (PropertyVetoException ex) {
1284  LOGGER.log(Level.WARNING, "Property Veto: ", ex); //NON-NLS
1285  }
1286  }
1287  // Another thread is needed because we have to wait for dataResult to populate
1288  }
1289 
1290  public void viewArtifactContent(BlackboardArtifact art) {
1291  new ViewContextAction(
1292  NbBundle.getMessage(this.getClass(), "DirectoryTreeTopComponent.action.viewArtContent.text"),
1293  new BlackboardArtifactNode(art)).actionPerformed(null);
1294  }
1295 
1296  public void addOnFinishedListener(PropertyChangeListener l) {
1297  DirectoryTreeTopComponent.this.addPropertyChangeListener(l);
1298  }
1299 
1300 }
static synchronized String getConfigSetting(String moduleName, String settingName)
static final Map< String, String > parsePath(String path)
void setSelectedNode(final String[] previouslySelectedNodePath, final String rootNodeName)
static String getTagsDisplayName()
Definition: Tags.java:78
static synchronized BankIdentificationNumber getBINInfo(int bin)
static synchronized boolean settingExists(String moduleName, String settingName)
static void setGroupItemsInTreeByDataSource(boolean value)
static synchronized void setConfigSetting(String moduleName, String settingName, String settingVal)
synchronized static Logger getLogger(String name)
Definition: Logger.java:124
static void addEventTypeSubscriber(Set< Events > eventTypes, PropertyChangeListener subscriber)
Definition: Case.java:486
static void addChangeListener(PreferenceChangeListener listener)
void setChildNodeSelectionInfo(NodeSelectionInfo selectedChildNodeInfo)
static synchronized DirectoryTreeTopComponent findInstance()

Copyright © 2012-2020 Basis Technology. Generated on: Wed Apr 8 2020
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.