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