diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.form b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.form index d8b83f1dff..d6c32623a4 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.form +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.form @@ -27,9 +27,6 @@ - - - diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java index 81d68c3029..3d62c1a8fc 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java @@ -26,8 +26,6 @@ import java.awt.Graphics; import java.awt.dnd.DnDConstants; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; -import java.beans.FeatureDescriptor; -import java.beans.PropertyChangeEvent; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; @@ -37,10 +35,9 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; -import java.util.prefs.Preferences; -import java.util.stream.Stream; import java.util.TreeSet; import java.util.prefs.Preferences; +import java.util.stream.Stream; import javax.swing.JTable; import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; @@ -49,8 +46,8 @@ import javax.swing.event.ListSelectionEvent; import javax.swing.event.TableColumnModelEvent; import javax.swing.event.TableColumnModelListener; import javax.swing.table.TableCellRenderer; - import javax.swing.table.TableColumnModel; +import org.apache.commons.lang3.StringUtils; import org.netbeans.swing.etable.ETableColumn; import org.netbeans.swing.outline.DefaultOutlineCellRenderer; import org.netbeans.swing.outline.DefaultOutlineModel; @@ -99,11 +96,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { private final PleasewaitNodeListener pleasewaitNodeListener = new PleasewaitNodeListener(); private Node currentRoot; - // When a column in the table is moved, these two variables keep track of where - // the column started and where it ended up. - private int startColumnIndex = -1; - private int endColumnIndex = -1; - private OutlineView ov; + /* * Convience reference to internal Outline. @@ -141,104 +134,10 @@ public class DataResultViewerTable extends AbstractDataResultViewer { outline.setDefaultRenderer(Object.class, new ColorTagCustomRenderer()); // add a listener so that when columns are moved, the new order is stored - outline.getColumnModel().addColumnModelListener(new TableColumnModelListener() { - - @Override - public void columnMoved(TableColumnModelEvent e) { - int fromIndex = e.getFromIndex(); - int toIndex = e.getToIndex(); - if (fromIndex == toIndex) { - return; - } - - /* - * Because a column may be dragged to several different - * positions before the mouse is released (thus causing multiple - * TableColumnModelEvents to be fired), we want to keep track of - * the starting column index in this potential series of - * movements. Therefore we only keep track of the original - * fromIndex in startColumnIndex, but we always update - * endColumnIndex to know the final position of the moved - * column. See the MouseListener mouseReleased method. - */ - if (startColumnIndex == -1) { - startColumnIndex = fromIndex; - } - endColumnIndex = toIndex; - - // This list contains the keys of propertiesMap in order - ArrayList indicesList = new ArrayList<>(propertiesMap.keySet()); - int leftIndex = Math.min(fromIndex, toIndex); - int rightIndex = Math.max(fromIndex, toIndex); - // Now we can copy the range of keys that have been affected by - // the column movement - List range = indicesList.subList(leftIndex, rightIndex + 1); - int rangeSize = range.size(); - - if (fromIndex < toIndex) { - // column moved right, shift all properties left, put in moved - // property at the rightmost index - Property movedProp = propertiesMap.get(range.get(0)); - for (int i = 0; i < rangeSize - 1; i++) { - propertiesMap.put(range.get(i), propertiesMap.get(range.get(i + 1))); - } - propertiesMap.put(range.get(rangeSize - 1), movedProp); - } else { - // column moved left, shift all properties right, put in moved - // property at the leftmost index - Property movedProp = propertiesMap.get(range.get(rangeSize - 1)); - for (int i = rangeSize - 1; i > 0; i--) { - propertiesMap.put(range.get(i), propertiesMap.get(range.get(i - 1))); - } - propertiesMap.put(range.get(0), movedProp); - } - - storeState(); - } - - @Override - public void columnAdded(TableColumnModelEvent e) { - } - - @Override - public void columnRemoved(TableColumnModelEvent e) { - } - - @Override - public void columnMarginChanged(ChangeEvent e) { - } - - @Override - public void columnSelectionChanged(ListSelectionEvent e) { - } - }); - - // add a listener to move columns back if user tries to move the first column out of place - outline.getTableHeader().addMouseListener(new MouseAdapter() { - @Override - public void mouseReleased(MouseEvent e) { - /* - * If the startColumnIndex is not -1 (which is the reset value), - * that means columns have been moved around. We then check to - * see if either the starting or end position is 0 (the first - * column), and then swap them back if that is the case because - * we don't want to allow movement of the first column. We then - * reset startColumnIndex to -1, the reset value. We check if - * startColumnIndex is at reset or not because it is possible - * for the mouse to be released and a MouseEvent to be fired - * without having moved any columns. - */ - if (startColumnIndex != -1 && (startColumnIndex == 0 || endColumnIndex == 0)) { - outline.moveColumn(endColumnIndex, startColumnIndex); - } - startColumnIndex = -1; - } - - @Override - public void mouseClicked(MouseEvent e) { - storeState(); - } - }); + TableListener tableListener = new TableListener(); + outline.getColumnModel().addColumnModelListener(tableListener); + // the listener also moves columns back if user tries to move the first column out of place + outline.getTableHeader().addMouseListener(tableListener); } /** @@ -251,9 +150,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { public void expandNode(Node n) { super.expandNode(n); - if (this.outlineView != null) { - outlineView.expandNode(n); - } + outlineView.expandNode(n); } /** @@ -267,12 +164,6 @@ public class DataResultViewerTable extends AbstractDataResultViewer { outlineView = new OutlineView(DataResultViewerTable.FIRST_COLUMN_LABEL); - outlineView.addComponentListener(new java.awt.event.ComponentAdapter() { - public void componentResized(java.awt.event.ComponentEvent evt) { - outlineViewComponentResized(evt); - } - }); - javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( @@ -285,42 +176,21 @@ public class DataResultViewerTable extends AbstractDataResultViewer { ); }// //GEN-END:initComponents - private void outlineViewComponentResized(java.awt.event.ComponentEvent evt) {//GEN-FIRST:event_outlineViewComponentResized - }//GEN-LAST:event_outlineViewComponentResized // Variables declaration - do not modify//GEN-BEGIN:variables private org.openide.explorer.view.OutlineView outlineView; // End of variables declaration//GEN-END:variables /** - * Gets properties from all children and, recursively, subchildren of Node. - * Note: won't work out the box for lazy load - you need to set all children - * props for the parent by hand - * - * JMTODO: This is 'search' goes depth first. Is that really what we want - * when getting child headers? - * - * @param node the Node to get properties for. - * - * @return A List of properties. - */ - static private List> getAllChildProperties(Node node) { - // This is a set because we add properties of up to 100 child nodes, and we want unique properties - Set> propertiesAcc = new LinkedHashSet<>(); - getAllChildPropertiesHelper(node, 100, propertiesAcc); - return new ArrayList<>(propertiesAcc); - } - - /** - * Helper to get the properties from all children and, recursively, - * subchildren of Node. Note: won't work out the box for lazy load - you - * need to set all children props for the parent by hand + * Get the properties from all children and, recursively, subchildren of + * Node. Note: won't work out the box for lazy load - you need to set all + * children props for the parent by hand * * @param parent Node with at least one child to get properties from * @param maxRows max number of rows to retrieve properties for (can * be used for memory optimization) * @param propertiesAcc Accumulator for properties. */ - static private void getAllChildPropertiesHelper(Node parent, int maxRows, Set> propertiesAcc) { + static private void getAllChildProperties(Node parent, int maxRows, Set> propertiesAcc) { Children children = parent.getChildren(); int childCount = 0; for (Node child : children.getNodes()) { @@ -330,7 +200,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { for (PropertySet ps : child.getPropertySets()) { propertiesAcc.addAll(Arrays.asList(ps.getProperties())); } - getAllChildPropertiesHelper(child, maxRows, propertiesAcc); + getAllChildProperties(child, maxRows, propertiesAcc); } } @@ -409,29 +279,22 @@ public class DataResultViewerTable extends AbstractDataResultViewer { Node.Property prop = props.remove(0); ((DefaultOutlineModel) outline.getOutlineModel()).setNodesColumnLabel(prop.getDisplayName()); } - - // Get the columns setup with respect to names and sortability - String[] propStrings = new String[props.size() * 2]; - for (int i = 0; i < props.size(); i++) { - final Property prop = props.get(i); - prop.setValue("ComparableColumnTTV", Boolean.TRUE); //NON-NLS - //First property column is sorted initially - if (i == 0) { - prop.setValue("TreeColumnTTV", Boolean.TRUE); // Identifies special property representing first (tree) column. NON-NLS - prop.setValue("SortingColumnTTV", Boolean.TRUE); // TreeTableView should be initially sorted by this property column. NON-NLS - } - propStrings[2 * i] = prop.getName(); - propStrings[2 * i + 1] = prop.getDisplayName(); - } - - outlineView.setPropertyColumns(propStrings); - - // show the horizontal scroll panel and show all the content & header - // If there is only one column (which was removed from props above) - // Just let the table resize itself. + /* + * show the horizontal scroll panel and show all the content & header If + * there is only one column (which was removed from props above) Just + * let the table resize itself. + */ outline.setAutoResizeMode((props.isEmpty()) ? JTable.AUTO_RESIZE_ALL_COLUMNS : JTable.AUTO_RESIZE_OFF); - if (root.getChildren().getNodesCount() != 0) { + assignColumns(props); + + setColumnWidths(); + + loadColumnSorting(); + } + + private void setColumnWidths() { + if (currentRoot.getChildren().getNodesCount() != 0) { final Graphics graphics = outlineView.getGraphics(); if (graphics != null) { final FontMetrics metrics = graphics.getFontMetrics(); @@ -465,15 +328,31 @@ public class DataResultViewerTable extends AbstractDataResultViewer { // if there's no content just auto resize all columns outline.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); } + } - loadSort(); + private void assignColumns(List> props) { + // Get the columns setup with respect to names and sortability + String[] propStrings = new String[props.size() * 2]; + for (int i = 0; i < props.size(); i++) { + final Property prop = props.get(i); + prop.setValue("ComparableColumnTTV", Boolean.TRUE); //NON-NLS + //First property column is sorted initially + if (i == 0) { + prop.setValue("TreeColumnTTV", Boolean.TRUE); // Identifies special property representing first (tree) column. NON-NLS + prop.setValue("SortingColumnTTV", Boolean.TRUE); // TreeTableView should be initially sorted by this property column. NON-NLS + } + propStrings[2 * i] = prop.getName(); + propStrings[2 * i + 1] = prop.getDisplayName(); + } + outlineView.setPropertyColumns(propStrings); } /** - * Store the current column order into a preference file. + * Store the current column order and sort information into a preference + * file. */ - private synchronized void storeState() { + private synchronized void storeColumnState() { if (currentRoot == null || propertiesMap.isEmpty()) { return; } @@ -485,13 +364,13 @@ public class DataResultViewerTable extends AbstractDataResultViewer { preferences.put(getColumnPositionKey(tfn.getColumnOrderKey(), entry.getValue().getName()), String.valueOf(entry.getKey())); } - final TableColumnModel columnModel = ov.getOutline().getColumnModel(); + final TableColumnModel columnModel = outline.getColumnModel(); //store the sorting information int numCols = columnModel.getColumnCount(); for (int i = 0; i < numCols; i++) { ETableColumn etc = (ETableColumn) columnModel.getColumn(i); - String columnName = ov.getOutline().getColumnName(i); + String columnName = outline.getColumnName(i); if (etc.isSorted()) { preferences.put(getColumnSortOrderKey(tfn.getColumnOrderKey(), columnName), String.valueOf(etc.isAscending())); preferences.put(getColumnSortRankKey(tfn.getColumnOrderKey(), columnName), String.valueOf(etc.getSortRank())); @@ -503,73 +382,60 @@ public class DataResultViewerTable extends AbstractDataResultViewer { } } - static private final class ColumnSortInfo { - - private final int modelIndex; - private final int rank; - private final boolean order; - - private ColumnSortInfo(int modelIndex, int rank, boolean order) { - this.modelIndex = modelIndex; - this.rank = rank; - this.order = order; - } - - private int getRank() { - return rank; - } - } - - private synchronized void loadSort() { + /** + * Reads and applies the column sorting information persisted to the + * preferences file. Must be called after loadColumnOrder() since it depends + * on propertiesMap being initialized, and before assignColumns since it + * cannot set the sort on columns that have not been added to the table. + */ + private synchronized void loadColumnSorting() { if (currentRoot == null || propertiesMap.isEmpty()) { return; } + if (currentRoot instanceof TableFilterNode) { - + final String columnKey = ((TableFilterNode) currentRoot).getColumnOrderKey(); final Preferences preferences = NbPreferences.forModule(DataResultViewerTable.class); - TableFilterNode tfn = (TableFilterNode) currentRoot; + //organize property sorting information, sorted by rank + TreeSet treeSet = new TreeSet<>(Comparator.comparing(ColumnSortInfo::getRank)); - if (currentRoot instanceof TableFilterNode) { - final String columnOrderKey = ((TableFilterNode) currentRoot).getColumnOrderKey(); + propertiesMap.entrySet().stream().forEach(entry -> { + final String propName = entry.getValue().getName(); + //if the sort rank is undefined, it will be defaulted to 0 => unsorted. + Integer sortRank = Integer.valueOf(preferences.get(getColumnSortRankKey(columnKey, propName), "0")); + //default to true => ascending + Boolean sortOrder = Boolean.valueOf(preferences.get(getColumnSortOrderKey(columnKey, propName), "true")); - final Preferences preferences = NbPreferences.forModule(DataResultViewerTable.class); - - //organize property sorting information, sorted by rank - TreeSet treeSet = new TreeSet<>(Comparator.comparing(ColumnSortInfo::getRank)); - propertiesMap.entrySet().stream() - .forEach(entry -> { - final String propName = entry.getValue().getName(); - //if the sort rank is undefined, it will be defaulted to 0 => unsorted. - Integer sortRank = Integer.valueOf(preferences.get(getColumnSortRankKey(columnOrderKey, propName), "0")); - //default to true => ascending - Boolean sortOrder = Boolean.valueOf(preferences.get(getColumnSortOrderKey(columnOrderKey, propName), "true")); - - treeSet.add(new ColumnSortInfo(entry.getKey(), sortRank, sortOrder)); - }); - - //apply sort information in rank order. - treeSet.forEach(sortInfo -> ov.getOutline().setColumnSorted(sortInfo.modelIndex, sortInfo.order, sortInfo.rank)); - } + treeSet.add(new ColumnSortInfo(entry.getKey(), sortRank, sortOrder)); + }); + //apply sort information in rank order. + treeSet.forEach(sortInfo -> outline.setColumnSorted(sortInfo.modelIndex, sortInfo.order, sortInfo.rank)); } } /** - * Loads the stored column order from the preference file. + * Gets a list of child properties (columns) in the order persisted in the + * preference file. Also initialized the propertiesMap with the column + * order. * - * @return a List> of the preferences in order + * @return a List> of the properties in the persisted + * order. */ private synchronized List> loadColumnOrder() { - List> props = getAllChildProperties(currentRoot); + // This is a set because we only want unique properties + Set> propertiesAcc = new LinkedHashSet<>(); + getAllChildProperties(currentRoot, 100, propertiesAcc); + List> props = new ArrayList<>(propertiesAcc); // If node is not table filter node, use default order for columns if (!(currentRoot instanceof TableFilterNode)) { - // The node is not a TableFilterNode, columns are going to be in default order return props; } - TableFilterNode tfn = (TableFilterNode) currentRoot; + final String columnKey = ((TableFilterNode) currentRoot).getColumnOrderKey(); propertiesMap.clear(); + /* * We load column index values into the properties map. If a property's * index is outside the range of the number of properties or the index @@ -582,7 +448,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { final Preferences preferences = NbPreferences.forModule(DataResultViewerTable.class); for (Property prop : props) { - Integer value = Integer.valueOf(preferences.get(getColumnPositionKey(tfn.getColumnOrderKey(), prop.getName()), "-1")); + Integer value = Integer.valueOf(preferences.get(getColumnPositionKey(columnKey, prop.getName()), "-1")); if (value >= 0 && value < offset && !propertiesMap.containsKey(value)) { propertiesMap.put(value, prop); noPreviousSettings = false; @@ -608,25 +474,46 @@ public class DataResultViewerTable extends AbstractDataResultViewer { * Gets a key for the current node and a property of its child nodes to * store the column position into a preference file. * - * @param prop Property of the column - * @param type The type of the current node + * @param keyBase The base of the key. Typically a fully quallified class + * name. + * @param propName The name of the specific property to make a key for. * * @return A generated key for the preference file */ - private String getColumnPositionKey(String type, String propName) { - return getColumnKeyBase(type, propName) + ".column"; + private String getColumnPositionKey(String keyBase, String propName) { + return getColumnKeyBase(keyBase, propName) + ".column"; } - private String getColumnSortOrderKey(String type, String propName) { - return getColumnKeyBase(type, propName) + ".sortOrder"; + /** + * Gets a key for the current node and a property of its child nodes to + * store the column sort ordering into a preference file. + * + * @param keyBase The base of the key. Typically a fully quallified class + * name. + * @param propName The name of the specific property to make a key for. + * + * @return A generated key for the preference file + */ + private String getColumnSortOrderKey(String keyBase, String propName) { + return getColumnKeyBase(keyBase, propName) + ".sortOrder"; } - private String getColumnSortRankKey(String type, String propName) { - return getColumnKeyBase(type, propName) + ".sortRank"; + /** + * Gets a key for the current node and a property of its child nodes to + * store the column sort rank into a preference file. + * + * @param keyBase The base of the key. Typically a fully quallified class + * name. + * @param propName The name of the specific property to make a key for. + * + * @return A generated key for the preference file + */ + private String getColumnSortRankKey(String keyBase, String propName) { + return getColumnKeyBase(keyBase, propName) + ".sortRank"; } - private static String getColumnKeyBase(String type, String propName) { - return stripNonAlphanumeric(type) + "." + stripNonAlphanumeric(propName); + private static String getColumnKeyBase(String keyBase, String propName) { + return stripNonAlphanumeric(keyBase) + "." + stripNonAlphanumeric(propName); } private static String stripNonAlphanumeric(String str) { @@ -653,6 +540,131 @@ public class DataResultViewerTable extends AbstractDataResultViewer { } + /** + * Encapsulates sorting information for a column to make loadSort simpler. + */ + static private final class ColumnSortInfo { + + private final int modelIndex; + private final int rank; + private final boolean order; + + private ColumnSortInfo(int modelIndex, int rank, boolean order) { + this.modelIndex = modelIndex; + this.rank = rank; + this.order = order; + } + + private int getRank() { + return rank; + } + } + + /** + * Listens to mouse events and table column events and persists column order + * and sorting changes + */ + private class TableListener extends MouseAdapter implements TableColumnModelListener { + + // When a column in the table is moved, these two variables keep track of where + // the column started and where it ended up. + private int startColumnIndex = -1; + private int endColumnIndex = -1; + + @Override + public void columnMoved(TableColumnModelEvent e) { + int fromIndex = e.getFromIndex(); + int toIndex = e.getToIndex(); + if (fromIndex == toIndex) { + return; + } + + /* + * Because a column may be dragged to several different positions + * before the mouse is released (thus causing multiple + * TableColumnModelEvents to be fired), we want to keep track of the + * starting column index in this potential series of movements. + * Therefore we only keep track of the original fromIndex in + * startColumnIndex, but we always update endColumnIndex to know the + * final position of the moved column. See the MouseListener + * mouseReleased method. + */ + if (startColumnIndex == -1) { + startColumnIndex = fromIndex; + } + endColumnIndex = toIndex; + + // This list contains the keys of propertiesMap in order + ArrayList indicesList = new ArrayList<>(propertiesMap.keySet()); + int leftIndex = Math.min(fromIndex, toIndex); + int rightIndex = Math.max(fromIndex, toIndex); + // Now we can copy the range of keys that have been affected by + // the column movement + List range = indicesList.subList(leftIndex, rightIndex + 1); + int rangeSize = range.size(); + + if (fromIndex < toIndex) { + // column moved right, shift all properties left, put in moved + // property at the rightmost index + Property movedProp = propertiesMap.get(range.get(0)); + for (int i = 0; i < rangeSize - 1; i++) { + propertiesMap.put(range.get(i), propertiesMap.get(range.get(i + 1))); + } + propertiesMap.put(range.get(rangeSize - 1), movedProp); + } else { + // column moved left, shift all properties right, put in moved + // property at the leftmost index + Property movedProp = propertiesMap.get(range.get(rangeSize - 1)); + for (int i = rangeSize - 1; i > 0; i--) { + propertiesMap.put(range.get(i), propertiesMap.get(range.get(i - 1))); + } + propertiesMap.put(range.get(0), movedProp); + } + + storeColumnState(); + } + + @Override + public void mouseReleased(MouseEvent e) { + /* + * If the startColumnIndex is not -1 (which is the reset value), + * that means columns have been moved around. We then check to see + * if either the starting or end position is 0 (the first column), + * and then swap them back if that is the case because we don't want + * to allow movement of the first column. We then reset + * startColumnIndex to -1, the reset value. We check if + * startColumnIndex is at reset or not because it is possible for + * the mouse to be released and a MouseEvent to be fired without + * having moved any columns. + */ + if (startColumnIndex != -1 && (startColumnIndex == 0 || endColumnIndex == 0)) { + outline.moveColumn(endColumnIndex, startColumnIndex); + } + startColumnIndex = -1; + } + + @Override + public void mouseClicked(MouseEvent e) { + storeColumnState(); + } + + @Override + public void columnAdded(TableColumnModelEvent e) { + } + + @Override + public void columnRemoved(TableColumnModelEvent e) { + } + + @Override + public void columnMarginChanged(ChangeEvent e) { + } + + @Override + public void columnSelectionChanged(ListSelectionEvent e) { + } + } + private class PleasewaitNodeListener extends NodeAdapter { private volatile boolean load = true; @@ -687,7 +699,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { * the outline table. This renderer colors a row if the tags property of the * node is not empty. */ - class ColorTagCustomRenderer extends DefaultOutlineCellRenderer { + private class ColorTagCustomRenderer extends DefaultOutlineCellRenderer { private static final long serialVersionUID = 1L;