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

Copyright © 2012-2018 Basis Technology. Generated on: Tue Dec 18 2018
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.