Autopsy  4.1
Graphical digital forensics platform for The Sleuth Kit and other tools.
DataResultViewerTable.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2013-2016 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.corecomponents;
20 
21 import java.awt.Color;
22 import java.awt.Component;
23 import java.awt.Cursor;
24 import java.awt.FontMetrics;
25 import java.awt.Graphics;
26 import java.awt.dnd.DnDConstants;
27 import java.awt.event.MouseAdapter;
28 import java.awt.event.MouseEvent;
29 import java.beans.PropertyChangeEvent;
30 import java.lang.reflect.InvocationTargetException;
31 import java.util.ArrayList;
32 import java.util.Arrays;
33 import java.util.LinkedHashSet;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.Set;
37 import java.util.TreeMap;
38 import javax.swing.JTable;
39 import javax.swing.ListSelectionModel;
40 import javax.swing.SwingUtilities;
41 import javax.swing.event.ChangeEvent;
42 import javax.swing.event.ListSelectionEvent;
43 import javax.swing.event.TableColumnModelEvent;
44 import javax.swing.event.TableColumnModelListener;
45 import javax.swing.table.TableCellRenderer;
46 import org.netbeans.swing.outline.DefaultOutlineCellRenderer;
47 import org.netbeans.swing.outline.DefaultOutlineModel;
48 import org.openide.explorer.ExplorerManager;
49 import org.openide.explorer.view.OutlineView;
50 import org.openide.nodes.AbstractNode;
51 import org.openide.nodes.Children;
52 import org.openide.nodes.Node;
53 import org.openide.nodes.Node.Property;
54 import org.openide.nodes.Node.PropertySet;
55 import org.openide.nodes.NodeEvent;
56 import org.openide.nodes.NodeListener;
57 import org.openide.nodes.NodeMemberEvent;
58 import org.openide.nodes.NodeReorderEvent;
59 import org.openide.util.NbBundle;
60 import org.openide.util.NbPreferences;
62 
66 // @@@ Restore implementation of DataResultViewerTable as a DataResultViewer
67 // service provider when DataResultViewers can be made compatible with node
68 // multiple selection actions.
69 //@ServiceProvider(service = DataResultViewer.class)
70 public class DataResultViewerTable extends AbstractDataResultViewer {
71 
72  private static final long serialVersionUID = 1L;
73 
74  private final String firstColumnLabel = NbBundle.getMessage(DataResultViewerTable.class, "DataResultViewerTable.firstColLbl");
75  /* The properties map maps
76  * key: stored value of column index -> value: property at that index
77  * We move around stored values instead of directly using the column indices
78  * in order to not override settings for a column that may not appear in the
79  * current table view due to its collection of its children's properties.
80  */
81  private final Map<Integer, Property<?>> propertiesMap = new TreeMap<>();
83  private static final String DUMMY_NODE_DISPLAY_NAME = NbBundle.getMessage(DataResultViewerTable.class, "DataResultViewerTable.dummyNodeDisplayName");
84  private static final Color TAGGED_COLOR = new Color(200, 210, 220);
85  private Node currentRoot;
86  // When a column in the table is moved, these two variables keep track of where
87  // the column started and where it ended up.
88  private int startColumnIndex = -1;
89  private int endColumnIndex = -1;
90 
97  public DataResultViewerTable(ExplorerManager explorerManager) {
98  super(explorerManager);
99  initialize();
100  }
101 
107  initialize();
108  }
109 
110  private void initialize() {
111  initComponents();
112 
113  OutlineView ov = ((OutlineView) this.tableScrollPanel);
114  ov.setAllowedDragActions(DnDConstants.ACTION_NONE);
115 
116  ov.getOutline().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
117 
118  // don't show the root node
119  ov.getOutline().setRootVisible(false);
120  ov.getOutline().setDragEnabled(false);
121 
122  // add a listener so that when columns are moved, the new order is stored
123  ov.getOutline().getColumnModel().addColumnModelListener(new TableColumnModelListener() {
124  @Override
125  public void columnAdded(TableColumnModelEvent e) {
126  }
127  @Override
128  public void columnRemoved(TableColumnModelEvent e) {
129  }
130  @Override
131  public void columnMarginChanged(ChangeEvent e) {
132  }
133  @Override
134  public void columnSelectionChanged(ListSelectionEvent e) {
135  }
136  @Override
137  public void columnMoved(TableColumnModelEvent e) {
138  int fromIndex = e.getFromIndex();
139  int toIndex = e.getToIndex();
140  if (fromIndex == toIndex) {
141  return;
142  }
143 
144  /* Because a column may be dragged to several different positions before
145  * the mouse is released (thus causing multiple TableColumnModelEvents to
146  * be fired), we want to keep track of the starting column index in this
147  * potential series of movements. Therefore we only keep track of the
148  * original fromIndex in startColumnIndex, but we always update
149  * endColumnIndex to know the final position of the moved column.
150  * See the MouseListener mouseReleased method.
151  */
152  if (startColumnIndex == -1) {
153  startColumnIndex = fromIndex;
154  }
155  endColumnIndex = toIndex;
156 
157  // This array contains the keys of propertiesMap in order
158  int[] indicesList = new int[propertiesMap.size()];
159  int pos = 0;
160  for (int key : propertiesMap.keySet()) {
161  indicesList[pos++] = key;
162  }
163  int leftIndex = Math.min(fromIndex, toIndex);
164  int rightIndex = Math.max(fromIndex, toIndex);
165  // Now we can copy the range of keys that have been affected by
166  // the column movement
167  int[] range = Arrays.copyOfRange(indicesList, leftIndex, rightIndex + 1);
168  int rangeSize = range.length;
169 
170  // column moved right, shift all properties left, put in moved
171  // property at the rightmost index
172  if (fromIndex < toIndex) {
173  Property<?> movedProp = propertiesMap.get(range[0]);
174  for (int i = 0; i < rangeSize - 1; i++) {
175  propertiesMap.put(range[i], propertiesMap.get(range[i + 1]));
176  }
177  propertiesMap.put(range[rangeSize - 1], movedProp);
178  }
179  // column moved left, shift all properties right, put in moved
180  // property at the leftmost index
181  else {
182  Property<?> movedProp = propertiesMap.get(range[rangeSize - 1]);
183  for (int i = rangeSize - 1; i > 0; i--) {
184  propertiesMap.put(range[i], propertiesMap.get(range[i - 1]));
185  }
186  propertiesMap.put(range[0], movedProp);
187  }
188 
189  storeState();
190  }
191  });
192 
193  // add a listener to move columns back if user tries to move the first column out of place
194  ov.getOutline().getTableHeader().addMouseListener(new MouseAdapter() {
195  @Override
196  public void mouseReleased(MouseEvent e) {
197  /* If the startColumnIndex is not -1 (which is the reset value), that
198  * means columns have been moved around. We then check to see if either
199  * the starting or end position is 0 (the first column), and then swap
200  * them back if that is the case because we don't want to allow movement
201  * of the first column. We then reset startColumnIndex to -1, the reset
202  * value.
203  * We check if startColumnIndex is at reset or not because it is
204  * possible for the mouse to be released and a MouseEvent to be fired
205  * without having moved any columns.
206  */
207  if (startColumnIndex != -1 && (startColumnIndex == 0 || endColumnIndex == 0)) {
208  ov.getOutline().moveColumn(endColumnIndex, startColumnIndex);
209  }
210  startColumnIndex = -1;
211  }
212  });
213  }
214 
220  @Override
221  public void expandNode(Node n) {
222  super.expandNode(n);
223 
224  if (this.tableScrollPanel != null) {
225  OutlineView ov = ((OutlineView) this.tableScrollPanel);
226  ov.expandNode(n);
227  }
228  }
229 
235  @SuppressWarnings("unchecked")
236  // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
237  private void initComponents() {
238 
239  tableScrollPanel = new OutlineView(this.firstColumnLabel);
240 
241  //new TreeTableView()
242  tableScrollPanel.addComponentListener(new java.awt.event.ComponentAdapter() {
243  public void componentResized(java.awt.event.ComponentEvent evt) {
245  }
246  });
247 
248  javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
249  this.setLayout(layout);
250  layout.setHorizontalGroup(
251  layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
252  .addComponent(tableScrollPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 691, Short.MAX_VALUE)
253  );
254  layout.setVerticalGroup(
255  layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
256  .addComponent(tableScrollPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 366, Short.MAX_VALUE)
257  );
258  }// </editor-fold>//GEN-END:initComponents
259 
260  private void tableScrollPanelComponentResized(java.awt.event.ComponentEvent evt) {//GEN-FIRST:event_tableScrollPanelComponentResized
261  }//GEN-LAST:event_tableScrollPanelComponentResized
262  // Variables declaration - do not modify//GEN-BEGIN:variables
263  private javax.swing.JScrollPane tableScrollPanel;
264  // End of variables declaration//GEN-END:variables
265 
275  private void getAllChildPropertyHeadersRec(Node parent, int rows, Set<Property<?>> propertiesAcc) {
276  Children children = parent.getChildren();
277  int childCount = 0;
278  for (Node child : children.getNodes()) {
279  if (++childCount > rows) {
280  return;
281  }
282  for (PropertySet ps : child.getPropertySets()) {
283  final Property<?>[] props = ps.getProperties();
284  final int propsNum = props.length;
285  for (int j = 0; j < propsNum; ++j) {
286  propertiesAcc.add(props[j]);
287  }
288  }
289  getAllChildPropertyHeadersRec(child, rows, propertiesAcc);
290  }
291  }
292 
293  @Override
294  public boolean isSupported(Node selectedNode) {
295  return true;
296  }
297 
304  @Override
305  public void setNode(Node selectedNode) {
306  final OutlineView ov = ((OutlineView) this.tableScrollPanel);
307  /* The quick filter must be reset because when determining column width,
308  * ETable.getRowCount is called, and the documentation states that quick
309  * filters must be unset for the method to work
310  * "If the quick-filter is applied the number of rows do not match the number of rows in the model."
311  */
312  ov.getOutline().unsetQuickFilter();
313  // change the cursor to "waiting cursor" for this operation
314  this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
315  try {
316  boolean hasChildren = false;
317  if (selectedNode != null) {
318  // @@@ This just did a DB round trip to get the count and the results were not saved...
319  hasChildren = selectedNode.getChildren().getNodesCount() > 0;
320  }
321 
322  Node oldNode = this.em.getRootContext();
323  if (oldNode != null) {
324  oldNode.removeNodeListener(dummyNodeListener);
325  }
326 
327  // if there's no selection node, do nothing
328  if (hasChildren) {
329  Node root = selectedNode;
330  dummyNodeListener.reset();
331  root.addNodeListener(dummyNodeListener);
332  setupTable(root);
333  } else {
334  Node emptyNode = new AbstractNode(Children.LEAF);
335  em.setRootContext(emptyNode); // make empty node
336  ov.getOutline().setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
337  ov.setPropertyColumns(); // set the empty property header
338  }
339  } finally {
340  this.setCursor(null);
341  }
342  }
343 
350  private void setupTable(final Node root) {
351 
352  em.setRootContext(root);
353  final OutlineView ov = ((OutlineView) this.tableScrollPanel);
354 
355  if (ov == null) {
356  return;
357  }
358  currentRoot = root;
359  List<Node.Property<?>> props = loadState();
360 
372  if (props.size() > 0) {
373  Node.Property<?> prop = props.remove(0);
374  ((DefaultOutlineModel) ov.getOutline().getOutlineModel()).setNodesColumnLabel(prop.getDisplayName());
375  }
376 
377  // Get the columns setup with respect to names and sortability
378  String[] propStrings = new String[props.size() * 2];
379  for (int i = 0; i < props.size(); i++) {
380  props.get(i).setValue("ComparableColumnTTV", Boolean.TRUE); //NON-NLS
381  //First property column is sorted initially
382  if (i == 0) {
383  props.get(i).setValue("TreeColumnTTV", Boolean.TRUE); // Identifies special property representing first (tree) column. NON-NLS
384  props.get(i).setValue("SortingColumnTTV", Boolean.TRUE); // TreeTableView should be initially sorted by this property column. NON-NLS
385  }
386  propStrings[2 * i] = props.get(i).getName();
387  propStrings[2 * i + 1] = props.get(i).getDisplayName();
388  }
389 
390  ov.setPropertyColumns(propStrings);
391 
392  // show the horizontal scroll panel and show all the content & header
393  // If there is only one column (which was removed from props above)
394  // Just let the table resize itself.
395  ov.getOutline().setAutoResizeMode((props.size() > 0) ? JTable.AUTO_RESIZE_OFF : JTable.AUTO_RESIZE_ALL_COLUMNS);
396 
397  if (root.getChildren().getNodesCount() != 0) {
398  final Graphics graphics = ov.getGraphics();
399  if (graphics != null) {
400  final FontMetrics metrics = graphics.getFontMetrics();
401 
402  int margin = 4;
403  int padding = 8;
404 
405  for (int column = 0; column < ov.getOutline().getModel().getColumnCount(); column++) {
406  int firstColumnPadding = (column == 0) ? 32 : 0;
407  int columnWidthLimit = (column == 0) ? 350 : 300;
408  int valuesWidth = 0;
409 
410  // find the maximum width needed to fit the values for the first 100 rows, at most
411  for (int row = 0; row < Math.min(100, ov.getOutline().getRowCount()); row++) {
412  TableCellRenderer renderer = ov.getOutline().getCellRenderer(row, column);
413  Component comp = ov.getOutline().prepareRenderer(renderer, row, column);
414  valuesWidth = Math.max(comp.getPreferredSize().width, valuesWidth);
415  }
416 
417  int headerWidth = metrics.stringWidth(ov.getOutline().getColumnName(column));
418  valuesWidth += firstColumnPadding; // add extra padding for first column
419 
420  int columnWidth = Math.max(valuesWidth, headerWidth);
421  columnWidth += 2 * margin + padding; // add margin and regular padding
422  columnWidth = Math.min(columnWidth, columnWidthLimit);
423 
424  ov.getOutline().getColumnModel().getColumn(column).setPreferredWidth(columnWidth);
425  }
426  }
427  } else {
428  // if there's no content just auto resize all columns
429  ov.getOutline().setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
430  }
431 
437  class ColorTagCustomRenderer extends DefaultOutlineCellRenderer {
438  private static final long serialVersionUID = 1L;
439  @Override
440  public Component getTableCellRendererComponent(JTable table,
441  Object value, boolean isSelected, boolean hasFocus, int row, int col) {
442 
443  Component component = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, col);
444  // only override the color if a node is not selected
445  if (!isSelected) {
446  Node node = currentRoot.getChildren().getNodeAt(table.convertRowIndexToModel(row));
447  boolean tagFound = false;
448  if (node != null) {
449  Node.PropertySet[] propSets = node.getPropertySets();
450  if (propSets.length != 0) {
451  // currently, a node has only one property set, named Sheet.PROPERTIES ("properties")
452  Node.Property<?>[] props = propSets[0].getProperties();
453  for (Property<?> prop : props) {
454  if (prop.getName().equals("Tags")) {
455  try {
456  tagFound = !prop.getValue().equals("");
457  } catch (IllegalAccessException | InvocationTargetException ignore) {
458  }
459  break;
460  }
461  }
462  }
463  }
464  //if the node does have associated tags, set its background color
465  if (tagFound) {
466  component.setBackground(TAGGED_COLOR);
467  }
468  }
469  return component;
470  }
471  }
472  ov.getOutline().setDefaultRenderer(Object.class, new ColorTagCustomRenderer());
473  }
474 
478  private synchronized void storeState() {
479  if (currentRoot == null || propertiesMap.isEmpty()) {
480  return;
481  }
482 
483  TableFilterNode tfn;
484  if (currentRoot instanceof TableFilterNode) {
485  tfn = (TableFilterNode) currentRoot;
486  } else {
487  return;
488  }
489 
490  // Store the current order of the columns into settings
491  for (Map.Entry<Integer, Property<?>> entry : propertiesMap.entrySet()) {
492  Property<?> prop = entry.getValue();
493  int storeValue = entry.getKey();
494  NbPreferences.forModule(this.getClass()).put(getColumnPreferenceKey(prop, tfn.getColumnOrderKey()), String.valueOf(storeValue));
495  }
496  }
497 
503  private synchronized List<Node.Property<?>> loadState() {
504  // This is a set because we add properties of up to 100 child nodes, and we want unique properties
505  Set<Property<?>> propertiesAcc = new LinkedHashSet<>();
506  this.getAllChildPropertyHeadersRec(currentRoot, 100, propertiesAcc);
507 
508  List<Node.Property<?>> props = new ArrayList<>(propertiesAcc);
509 
510  // If node is not table filter node, use default order for columns
511  TableFilterNode tfn;
512  if (currentRoot instanceof TableFilterNode) {
513  tfn = (TableFilterNode) currentRoot;
514  } else {
515  // The node is not a TableFilterNode, columns are going to be in default order
516  return props;
517  }
518 
519  propertiesMap.clear();
520  /*
521  * We load column index values into the properties map. If a property's
522  * index is outside the range of the number of properties or the index
523  * has already appeared as the position of another property, we put that
524  * property at the end.
525  */
526  int offset = props.size();
527  boolean noPreviousSettings = true;
528  for (Property<?> prop : props) {
529  Integer value = Integer.valueOf(NbPreferences.forModule(this.getClass()).get(getColumnPreferenceKey(prop, tfn.getColumnOrderKey()), "-1"));
530  if (value >= 0 && value < offset && !propertiesMap.containsKey(value)) {
531  propertiesMap.put(value, prop);
532  noPreviousSettings = false;
533  } else {
534  propertiesMap.put(offset, prop);
535  offset++;
536  }
537  }
538 
539  // If none of the properties had previous settings, we should decrement
540  // each value by the number of properties to make the values 0-indexed.
541  if (noPreviousSettings) {
542  Integer[] keys = propertiesMap.keySet().toArray(new Integer[propertiesMap.keySet().size()]);
543  for (int key : keys) {
544  propertiesMap.put(key - props.size(), propertiesMap.get(key));
545  propertiesMap.remove(key);
546  }
547  }
548 
549  return new ArrayList<>(propertiesMap.values());
550  }
551 
560  private String getColumnPreferenceKey(Property<?> prop, String type) {
561  return type.replaceAll("[^a-zA-Z0-9_]", "") + "."
562  + prop.getName().replaceAll("[^a-zA-Z0-9_]", "") + ".column";
563  }
564 
565  @Override
566  public String getTitle() {
567  return NbBundle.getMessage(this.getClass(), "DataResultViewerTable.title");
568  }
569 
570  @Override
572  return new DataResultViewerTable();
573  }
574 
575  @Override
576  public void clearComponent() {
577  this.tableScrollPanel.removeAll();
578  this.tableScrollPanel = null;
579 
580  super.clearComponent();
581  }
582 
583  private class DummyNodeListener implements NodeListener {
584 
585  private volatile boolean load = true;
586 
587  public void reset() {
588  load = true;
589  }
590 
591  @Override
592  public void childrenAdded(final NodeMemberEvent nme) {
593  Node[] delta = nme.getDelta();
594  if (load && containsReal(delta)) {
595  load = false;
596  if (SwingUtilities.isEventDispatchThread()) {
597  setupTable(nme.getNode());
598  } else {
599  SwingUtilities.invokeLater(() -> {
600  setupTable(nme.getNode());
601  });
602  }
603  }
604  }
605 
606  private boolean containsReal(Node[] delta) {
607  for (Node n : delta) {
608  if (!n.getDisplayName().equals(DUMMY_NODE_DISPLAY_NAME)) {
609  return true;
610  }
611  }
612  return false;
613  }
614 
615  @Override
616  public void childrenRemoved(NodeMemberEvent nme) {
617  }
618 
619  @Override
620  public void childrenReordered(NodeReorderEvent nre) {
621  }
622 
623  @Override
624  public void nodeDestroyed(NodeEvent ne) {
625  }
626 
627  @Override
628  public void propertyChange(PropertyChangeEvent evt) {
629  }
630  }
631 }
void tableScrollPanelComponentResized(java.awt.event.ComponentEvent evt)
void getAllChildPropertyHeadersRec(Node parent, int rows, Set< Property<?>> propertiesAcc)

Copyright © 2012-2016 Basis Technology. Generated on: Mon Jan 2 2017
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.