Merge branch 'develop' of https://github.com/sleuthkit/autopsy into vm_detection

This commit is contained in:
Eugene Livis 2016-01-27 13:00:14 -05:00
commit 013a85919f
16 changed files with 760 additions and 399 deletions

View File

@ -228,6 +228,13 @@ public abstract class AbstractAbstractFileNode<T extends AbstractFile> extends A
public String toString() { public String toString() {
return NbBundle.getMessage(this.getClass(), "AbstractAbstractFileNode.objectId"); return NbBundle.getMessage(this.getClass(), "AbstractAbstractFileNode.objectId");
}
},
MIMETYPE {
@Override
public String toString() {
return NbBundle.getMessage(this.getClass(), "AbstractAbstractFileNode.mimeType");
} }
}, },
} }
@ -235,8 +242,8 @@ public abstract class AbstractAbstractFileNode<T extends AbstractFile> extends A
/** /**
* Fill map with AbstractFile properties * Fill map with AbstractFile properties
* *
* @param map map with preserved ordering, where property names/values * @param map map with preserved ordering, where property names/values are
* are put * put
* @param content to extract properties from * @param content to extract properties from
*/ */
public static void fillPropertyMap(Map<String, Object> map, AbstractFile content) { public static void fillPropertyMap(Map<String, Object> map, AbstractFile content) {
@ -268,6 +275,7 @@ public abstract class AbstractAbstractFileNode<T extends AbstractFile> extends A
map.put(AbstractFilePropertyType.HASHSETS.toString(), getHashSetHitsForFile(content)); map.put(AbstractFilePropertyType.HASHSETS.toString(), getHashSetHitsForFile(content));
map.put(AbstractFilePropertyType.MD5HASH.toString(), content.getMd5Hash() == null ? "" : content.getMd5Hash()); map.put(AbstractFilePropertyType.MD5HASH.toString(), content.getMd5Hash() == null ? "" : content.getMd5Hash());
map.put(AbstractFilePropertyType.ObjectID.toString(), content.getId()); map.put(AbstractFilePropertyType.ObjectID.toString(), content.getId());
map.put(AbstractFilePropertyType.MIMETYPE.toString(), content.getMIMEType() == null ? "" : content.getMIMEType());
} }
static String getContentDisplayName(AbstractFile file) { static String getContentDisplayName(AbstractFile file) {

View File

@ -18,6 +18,7 @@ AbstractAbstractFileNode.typeMetaColLbl=Type(Meta)
AbstractAbstractFileNode.knownColLbl=Known AbstractAbstractFileNode.knownColLbl=Known
AbstractAbstractFileNode.inHashsetsColLbl=In Hashsets AbstractAbstractFileNode.inHashsetsColLbl=In Hashsets
AbstractAbstractFileNode.md5HashColLbl=MD5 Hash AbstractAbstractFileNode.md5HashColLbl=MD5 Hash
AbstractAbstractFileNode.mimeType = MIME Type
AbstractContentChildren.CreateTSKNodeVisitor.exception.noNodeMsg=No Node defined for the given SleuthkitItem AbstractContentChildren.CreateTSKNodeVisitor.exception.noNodeMsg=No Node defined for the given SleuthkitItem
AbstractContentChildren.createAutopsyNodeVisitor.exception.noNodeMsg=No Node defined for the given DisplayableItem AbstractContentChildren.createAutopsyNodeVisitor.exception.noNodeMsg=No Node defined for the given DisplayableItem
AbstractContentNode.exception.cannotChangeSysName.msg=Cannot change the system name. AbstractContentNode.exception.cannotChangeSysName.msg=Cannot change the system name.

View File

@ -23,9 +23,9 @@ import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.util.logging.Level; import java.util.logging.Level;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
/** /**
* This class supports programmer productivity by abstracting frequently used * This class supports programmer productivity by abstracting frequently used
* code to load FXML-defined GUI components, * code to load FXML-defined GUI components,
@ -38,7 +38,7 @@ public class FXMLConstructor {
private static Logger logger = Logger.getLogger(FXMLConstructor.class.getName()); private static Logger logger = Logger.getLogger(FXMLConstructor.class.getName());
static public void construct(Node n, String fxmlFileName) { static public void construct(Object n, String fxmlFileName) {
final String name = "nbres:/" + StringUtils.replace(n.getClass().getPackage().getName(), ".", "/") + "/" + fxmlFileName; final String name = "nbres:/" + StringUtils.replace(n.getClass().getPackage().getName(), ".", "/") + "/" + fxmlFileName;
try { try {

View File

@ -75,7 +75,6 @@ import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState;
import org.sleuthkit.autopsy.imagegallery.gui.NoGroupsDialog; import org.sleuthkit.autopsy.imagegallery.gui.NoGroupsDialog;
import org.sleuthkit.autopsy.imagegallery.gui.Toolbar; import org.sleuthkit.autopsy.imagegallery.gui.Toolbar;
import org.sleuthkit.autopsy.imagegallery.gui.navpanel.NavPanel;
import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.ingest.IngestManager;
import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardArtifact;
@ -94,6 +93,7 @@ import org.sleuthkit.datamodel.TskData;
public final class ImageGalleryController implements Executor { public final class ImageGalleryController implements Executor {
private final Executor execDelegate = Executors.newSingleThreadExecutor(); private final Executor execDelegate = Executors.newSingleThreadExecutor();
private Runnable showTree;
@Override @Override
public void execute(Runnable command) { public void execute(Runnable command) {
@ -153,7 +153,7 @@ public final class ImageGalleryController implements Executor {
private Node infoOverlay; private Node infoOverlay;
private SleuthkitCase sleuthKitCase; private SleuthkitCase sleuthKitCase;
private NavPanel navPanel; // private NavPanel navPanel;
public ReadOnlyBooleanProperty getMetaDataCollapsed() { public ReadOnlyBooleanProperty getMetaDataCollapsed() {
return metaDataCollapsed.getReadOnlyProperty(); return metaDataCollapsed.getReadOnlyProperty();
@ -266,8 +266,8 @@ public final class ImageGalleryController implements Executor {
@ThreadConfined(type = ThreadConfined.ThreadType.ANY) @ThreadConfined(type = ThreadConfined.ThreadType.ANY)
public void advance(GroupViewState newState, boolean forceShowTree) { public void advance(GroupViewState newState, boolean forceShowTree) {
if (Objects.nonNull(navPanel) && forceShowTree) { if (forceShowTree && showTree != null) {
navPanel.showTree(); showTree.run();
} }
historyManager.advance(newState); historyManager.advance(newState);
} }
@ -482,8 +482,8 @@ public final class ImageGalleryController implements Executor {
return tagsManager; return tagsManager;
} }
public void setNavPanel(NavPanel navPanel) { public void setShowTree(Runnable showTree) {
this.navPanel = navPanel; this.showTree = showTree;
} }
public UndoRedoManager getUndoManager() { public UndoRedoManager getUndoManager() {

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2013 Basis Technology Corp. * Copyright 2013-16 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org * Contact: carrier <at> sleuthkit <dot> org
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -23,7 +23,9 @@ import javafx.application.Platform;
import javafx.embed.swing.JFXPanel; import javafx.embed.swing.JFXPanel;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.control.SplitPane; import javafx.scene.control.SplitPane;
import javafx.scene.control.TabPane;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import org.netbeans.api.settings.ConvertAsProperties; import org.netbeans.api.settings.ConvertAsProperties;
@ -40,7 +42,8 @@ import org.sleuthkit.autopsy.imagegallery.gui.SummaryTablePane;
import org.sleuthkit.autopsy.imagegallery.gui.Toolbar; import org.sleuthkit.autopsy.imagegallery.gui.Toolbar;
import org.sleuthkit.autopsy.imagegallery.gui.drawableviews.GroupPane; import org.sleuthkit.autopsy.imagegallery.gui.drawableviews.GroupPane;
import org.sleuthkit.autopsy.imagegallery.gui.drawableviews.MetaDataPane; import org.sleuthkit.autopsy.imagegallery.gui.drawableviews.MetaDataPane;
import org.sleuthkit.autopsy.imagegallery.gui.navpanel.NavPanel; import org.sleuthkit.autopsy.imagegallery.gui.navpanel.GroupTree;
import org.sleuthkit.autopsy.imagegallery.gui.navpanel.HashHitGroupList;
/** /**
* Top component which displays ImageGallery interface. * Top component which displays ImageGallery interface.
@ -92,7 +95,7 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl
} }
public static void closeTopComponent() { public static void closeTopComponent() {
if(topComponentInitialized){ if (topComponentInitialized) {
final TopComponent etc = WindowManager.getDefault().findTopComponent("ImageGalleryTopComponent"); final TopComponent etc = WindowManager.getDefault().findTopComponent("ImageGalleryTopComponent");
if (etc != null) { if (etc != null) {
try { try {
@ -122,7 +125,8 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl
private GroupPane groupPane; private GroupPane groupPane;
private NavPanel navPanel; private GroupTree groupTree;
private HashHitGroupList hashHitList;
private VBox leftPane; private VBox leftPane;
@ -149,8 +153,13 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl
metaDataTable = new MetaDataPane(controller); metaDataTable = new MetaDataPane(controller);
navPanel = new NavPanel(controller); groupTree = new GroupTree(controller);
leftPane = new VBox(navPanel, new SummaryTablePane(controller)); hashHitList = new HashHitGroupList(controller);
TabPane tabPane = new TabPane(groupTree, hashHitList);
VBox.setVgrow(tabPane, Priority.ALWAYS);
leftPane = new VBox(tabPane, new SummaryTablePane(controller));
SplitPane.setResizableWithParent(leftPane, Boolean.FALSE); SplitPane.setResizableWithParent(leftPane, Boolean.FALSE);
SplitPane.setResizableWithParent(groupPane, Boolean.TRUE); SplitPane.setResizableWithParent(groupPane, Boolean.TRUE);
SplitPane.setResizableWithParent(metaDataTable, Boolean.FALSE); SplitPane.setResizableWithParent(metaDataTable, Boolean.FALSE);
@ -158,7 +167,7 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl
splitPane.setDividerPositions(0.0, 1.0); splitPane.setDividerPositions(0.0, 1.0);
ImageGalleryController.getDefault().setStacks(fullUIStack, centralStack); ImageGalleryController.getDefault().setStacks(fullUIStack, centralStack);
ImageGalleryController.getDefault().setNavPanel(navPanel); ImageGalleryController.getDefault().setShowTree(() -> tabPane.getSelectionModel().select(groupTree));
}); });
} }

View File

@ -39,6 +39,8 @@ import java.util.stream.Stream;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper; import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import static javafx.concurrent.Worker.State.CANCELLED; import static javafx.concurrent.Worker.State.CANCELLED;
@ -116,10 +118,13 @@ public class GroupManager {
* --- current grouping/sorting attributes --- * --- current grouping/sorting attributes ---
*/ */
private volatile GroupSortBy sortBy = GroupSortBy.NONE; private volatile GroupSortBy sortBy = GroupSortBy.NONE;
private volatile DrawableAttribute<?> groupBy = DrawableAttribute.PATH; private volatile DrawableAttribute<?> groupBy = DrawableAttribute.PATH;
private volatile SortOrder sortOrder = SortOrder.ASCENDING; private volatile SortOrder sortOrder = SortOrder.ASCENDING;
private final ReadOnlyObjectWrapper<GroupSortBy> sortByProp = new ReadOnlyObjectWrapper<>(sortBy);
private final ReadOnlyObjectWrapper< DrawableAttribute<?>> groupByProp = new ReadOnlyObjectWrapper<>(groupBy);
private final ReadOnlyObjectWrapper<SortOrder> sortOrderProp = new ReadOnlyObjectWrapper<>(sortOrder);
private final ReadOnlyDoubleWrapper regroupProgress = new ReadOnlyDoubleWrapper(); private final ReadOnlyDoubleWrapper regroupProgress = new ReadOnlyDoubleWrapper();
public void setDB(DrawableDB db) { public void setDB(DrawableDB db) {
@ -424,24 +429,39 @@ public class GroupManager {
return sortBy; return sortBy;
} }
public void setSortBy(GroupSortBy sortBy) { void setSortBy(GroupSortBy sortBy) {
this.sortBy = sortBy; this.sortBy = sortBy;
Platform.runLater(() -> sortByProp.set(sortBy));
}
public ReadOnlyObjectProperty<GroupSortBy> getSortByProperty() {
return sortByProp.getReadOnlyProperty();
} }
public DrawableAttribute<?> getGroupBy() { public DrawableAttribute<?> getGroupBy() {
return groupBy; return groupBy;
} }
public void setGroupBy(DrawableAttribute<?> groupBy) { void setGroupBy(DrawableAttribute<?> groupBy) {
this.groupBy = groupBy; this.groupBy = groupBy;
Platform.runLater(() -> groupByProp.set(groupBy));
}
public ReadOnlyObjectProperty<DrawableAttribute<?>> getGroupByProperty() {
return groupByProp.getReadOnlyProperty();
} }
public SortOrder getSortOrder() { public SortOrder getSortOrder() {
return sortOrder; return sortOrder;
} }
public void setSortOrder(SortOrder sortOrder) { void setSortOrder(SortOrder sortOrder) {
this.sortOrder = sortOrder; this.sortOrder = sortOrder;
Platform.runLater(() -> sortOrderProp.set(sortOrder));
}
public ReadOnlyObjectProperty<SortOrder> getSortOrderProperty() {
return sortOrderProp.getReadOnlyProperty();
} }
/** /**

View File

@ -0,0 +1,82 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2013-16 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.imagegallery.gui.navpanel;
import com.google.common.collect.ImmutableList;
import java.util.Comparator;
import java.util.function.Function;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup;
/**
*
*/
final class GroupComparators<T extends Comparable<T>> implements Comparator<DrawableGroup> {
static final GroupComparators<Long> UNCATEGORIZED_COUNT =
new GroupComparators<>("Uncategorized Count", DrawableGroup::getUncategorizedCount, String::valueOf, false);
static final GroupComparators<String> ALPHABETICAL =
new GroupComparators<>("Group Name", DrawableGroup::getGroupByValueDislpayName, String::valueOf, false);
static final GroupComparators<Long> HIT_COUNT =
new GroupComparators<>("Hit Count", DrawableGroup::getHashSetHitsCount, String::valueOf, true);
static final GroupComparators<Integer> FILE_COUNT =
new GroupComparators<>("Group Size", DrawableGroup::getSize, String::valueOf, true);
static final GroupComparators<Double> HIT_FILE_RATIO =
new GroupComparators<>("Hit Density", DrawableGroup::getHashHitDensity, density -> String.format("%.2f", density) + "%", true);
private final static ImmutableList<GroupComparators<?>> values = ImmutableList.of(UNCATEGORIZED_COUNT, ALPHABETICAL, HIT_COUNT, FILE_COUNT, HIT_FILE_RATIO);
public static ImmutableList<GroupComparators<?>> getValues() {
return values;
}
private final Function<DrawableGroup, T> extractor;
private final Function<T, String> valueFormatter;
private final boolean orderReveresed;
private final String displayName;
private GroupComparators(String displayName, Function<DrawableGroup, T> extractor, Function<T, String> formatter, boolean defaultOrderReversed) {
this.displayName = displayName;
this.extractor = extractor;
this.orderReveresed = defaultOrderReversed;
this.valueFormatter = formatter;
}
@Override
public int compare(DrawableGroup o1, DrawableGroup o2) {
int compareTo = extractor.apply(o1).compareTo(extractor.apply(o2));
return orderReveresed ? -compareTo : compareTo;
}
public String getDisplayName() {
return displayName;
}
@Override
public String toString() {
return displayName;
}
String getFormattedValueOfGroup(DrawableGroup group) {
return valueFormatter.apply(extractor.apply(group));
}
}

View File

@ -0,0 +1,177 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2015 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.imagegallery.gui.navpanel;
import static java.util.Objects.isNull;
import java.util.Optional;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.scene.control.ListCell;
import javafx.scene.control.OverrunStyle;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javax.annotation.Nonnull;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup;
/**
*
*/
class GroupListCell extends ListCell<DrawableGroup> {
/**
* icon to use if this cell's TreeNode doesn't represent a group but just a
* folder(with no DrawableFiles) in the file system hierarchy.
*/
private static final Image EMPTY_FOLDER_ICON =
new Image(GroupTreeCell.class.getResourceAsStream("/org/sleuthkit/autopsy/imagegallery/images/folder.png"));
/**
* reference to group files listener that allows us to remove it from a
* group when a new group is assigned to this Cell
*/
private final InvalidationListener fileCountListener = (Observable o) -> {
final String text = getGroupName() + getCountsText();
Platform.runLater(() -> {
setText(text);
setTooltip(new Tooltip(text));
});
};
/**
* reference to group seen listener that allows us to remove it from a group
* when a new group is assigned to this Cell
*/
private final InvalidationListener seenListener = (Observable o) -> {
final String style = getSeenStyleClass();
Platform.runLater(() -> {
setStyle(style);
});
};
private final ReadOnlyObjectProperty<GroupComparators<?>> sortOrder;
GroupListCell(ReadOnlyObjectProperty<GroupComparators<?>> sortOrderProperty) {
this.sortOrder = sortOrderProperty;
getStylesheets().add(GroupTreeCell.class.getResource("GroupTreeCell.css").toExternalForm());
getStyleClass().add("groupTreeCell"); //reduce indent to 5, default is 10 which uses up a lot of space.
//since end of path is probably more interesting put ellipsis at front
setTextOverrun(OverrunStyle.LEADING_ELLIPSIS);
Platform.runLater(() -> {
prefWidthProperty().bind(getListView().widthProperty().subtract(15));
});
}
/**
* {@inheritDoc }
*/
@Override
protected synchronized void updateItem(final DrawableGroup group, boolean empty) {
//if there was a previous group, remove the listeners
Optional.ofNullable(getItem())
.ifPresent(oldGroup -> {
sortOrder.removeListener(fileCountListener);
oldGroup.fileIds().removeListener(fileCountListener);
oldGroup.seenProperty().removeListener(seenListener);
oldGroup.uncatCountProperty().removeListener(fileCountListener);
});
super.updateItem(group, empty);
if (isNull(group) || empty) {
Platform.runLater(() -> {
setTooltip(null);
setText(null);
setGraphic(null);
setStyle("");
});
} else {
if (isNull(group)) {
final String text = getGroupName();
//"dummy" group in file system tree <=> a folder with no drawables
Platform.runLater(() -> {
setTooltip(new Tooltip(text));
setText(text);
setGraphic(new ImageView(EMPTY_FOLDER_ICON));
setStyle("");
});
} else {
//if number of files in this group changes (eg a file is recategorized), update counts via listener
group.fileIds().addListener(fileCountListener);
group.uncatCountProperty().addListener(fileCountListener);
sortOrder.addListener(fileCountListener);
//if the seen state of this group changes update its style
group.seenProperty().addListener(seenListener);
//and use icon corresponding to group type
final Image icon = group.groupKey.getAttribute().getIcon();
final String text = getGroupName() + getCountsText();
final String style = getSeenStyleClass();
Platform.runLater(() -> {
setTooltip(new Tooltip(text));
setGraphic(new ImageView(icon));
setText(text);
setStyle(style);
});
}
}
}
private String getGroupName() {
return Optional.ofNullable(getItem())
.map(group -> group.getGroupByValueDislpayName())
.orElse("");
}
/**
* return the styleClass to apply based on the assigned group's seen status
*
* @return the style class to apply
*/
@Nonnull
private String getSeenStyleClass() {
return Optional.ofNullable(getItem())
.map(DrawableGroup::isSeen)
.map(seen -> seen ? "" : "-fx-font-weight:bold;")
.orElse(""); //if item is null or group is null
}
/**
* get the counts part of the text to apply to this cell, including
* parentheses
*
* @return get the counts part of the text to apply to this cell
*/
@Nonnull
private String getCountsText() {
return Optional.ofNullable(getItem())
.map(group ->
" (" + (sortOrder.get() == GroupComparators.ALPHABETICAL
? group.getSize()
: sortOrder.get().getFormattedValueOfGroup(group)) + ")"
).orElse(""); //if item is null or group is null
}
}

View File

@ -0,0 +1,149 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2016 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.imagegallery.gui.navpanel;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.collections.ListChangeListener;
import javafx.fxml.FXML;
import javafx.scene.control.SelectionModel;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.image.ImageView;
import org.apache.commons.lang3.StringUtils;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.imagegallery.FXMLConstructor;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup;
/**
* Shows path based groups as a tree and others kinds of groups as a flat list (
* a tree with an invisible root and only one level of children). Shows controls
* to adjust the sorting only in flat list mode.
*/
final public class GroupTree extends NavPanel<TreeItem<GroupTreeNode>> {
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private final GroupTreeItem groupTreeRoot = new GroupTreeItem("", null, true);
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private final TreeView<GroupTreeNode> groupTree = new TreeView<>(groupTreeRoot);
public GroupTree(ImageGalleryController controller) {
super(controller);
FXMLConstructor.construct(this, "NavPanel.fxml");
}
@FXML
@Override
void initialize() {
super.initialize();
setText("All Groups");
setGraphic(new ImageView("org/sleuthkit/autopsy/imagegallery/images/Folder-icon.png"));
getBorderPane().setCenter(groupTree);
//only show sorting controls if not grouping by path
BooleanBinding groupedByPath = Bindings.equal(getGroupManager().getGroupByProperty(), DrawableAttribute.PATH);
getToolBar().visibleProperty().bind(groupedByPath.not());
getToolBar().managedProperty().bind(groupedByPath.not());
groupTree.setCellFactory(treeView -> new GroupTreeCell(getSortByBox().getSelectionModel().selectedItemProperty()));
groupTree.setShowRoot(false);
getGroupManager().getAnalyzedGroups().addListener((ListChangeListener.Change<? extends DrawableGroup> change) -> {
while (change.next()) {
change.getAddedSubList().stream().forEach(this::insertGroup);
change.getRemoved().stream().forEach(this::removeFromTree);
}
});
for (DrawableGroup g : getGroupManager().getAnalyzedGroups()) {
insertGroup(g);
}
}
/**
* Set the tree to the passed in group
*
* @param grouping
*/
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
@Override
void setFocusedGroup(DrawableGroup grouping) {
final GroupTreeItem treeItemForGroup = groupTreeRoot.getTreeItemForPath(groupingToPath(grouping));
if (treeItemForGroup != null) {
groupTree.getSelectionModel().select(treeItemForGroup);
Platform.runLater(() -> {
int row = groupTree.getRow(treeItemForGroup);
if (row != -1) {
groupTree.scrollTo(row - 2); //put newly selected row 3 from the top
}
});
}
}
@Override
Function<TreeItem<GroupTreeNode>, DrawableGroup> getDataItemMapper() {
return treeItem -> treeItem.getValue().getGroup();
}
@Override
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
SelectionModel<TreeItem<GroupTreeNode>> getSelectionModel() {
return groupTree.getSelectionModel();
}
@Override
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
void applyGroupComparator() {
groupTreeRoot.resortChildren(getComparator());
}
@Override
GroupComparators<String> getDefaultComparator() {
return GroupComparators.ALPHABETICAL;
}
private void insertGroup(DrawableGroup g) {
groupTreeRoot.insert(groupingToPath(g), g, true);
}
private void removeFromTree(DrawableGroup g) {
Optional.ofNullable(groupTreeRoot.getTreeItemForGroup(g))
.ifPresent(GroupTreeItem::removeFromParent);
}
private static List<String> groupingToPath(DrawableGroup g) {
String path = g.groupKey.getValueDisplayName();
if (g.groupKey.getAttribute() != DrawableAttribute.PATH) {
String stripStart = StringUtils.strip(path, "/");
return Arrays.asList(stripStart);
} else {
String[] cleanPathTokens = StringUtils.stripStart(path, "/").split("/");
return Arrays.asList(cleanPathTokens);
}
}
}

View File

@ -40,7 +40,7 @@ import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup;
* TODO: we should use getStyleClass().add() rather than setStyle but it didn't * TODO: we should use getStyleClass().add() rather than setStyle but it didn't
* seem to work properly * seem to work properly
*/ */
class GroupTreeCell extends TreeCell<TreeNode> { class GroupTreeCell extends TreeCell<GroupTreeNode> {
/** /**
* icon to use if this cell's TreeNode doesn't represent a group but just a * icon to use if this cell's TreeNode doesn't represent a group but just a
@ -71,11 +71,9 @@ class GroupTreeCell extends TreeCell<TreeNode> {
setStyle(style); setStyle(style);
}); });
}; };
private final ReadOnlyObjectProperty<GroupComparators<?>> sortOrder;
private final ReadOnlyObjectProperty<TreeNodeComparator<?>> sortOrder; GroupTreeCell(ReadOnlyObjectProperty<GroupComparators<?>> sortOrderProperty) {
GroupTreeCell(ReadOnlyObjectProperty<TreeNodeComparator<?>> sortOrderProperty) {
this.sortOrder = sortOrderProperty; this.sortOrder = sortOrderProperty;
getStylesheets().add(GroupTreeCell.class.getResource("GroupTreeCell.css").toExternalForm()); getStylesheets().add(GroupTreeCell.class.getResource("GroupTreeCell.css").toExternalForm());
getStyleClass().add("groupTreeCell"); //reduce indent to 5, default is 10 which uses up a lot of space. getStyleClass().add("groupTreeCell"); //reduce indent to 5, default is 10 which uses up a lot of space.
@ -92,12 +90,12 @@ class GroupTreeCell extends TreeCell<TreeNode> {
* {@inheritDoc } * {@inheritDoc }
*/ */
@Override @Override
protected synchronized void updateItem(final TreeNode treeNode, boolean empty) { protected synchronized void updateItem(final GroupTreeNode treeNode, boolean empty) {
//if there was a previous group, remove the listeners //if there was a previous group, remove the listeners
Optional.ofNullable(getItem()) Optional.ofNullable(getItem())
.map(TreeNode::getGroup) .map(GroupTreeNode::getGroup)
.ifPresent(group -> { .ifPresent(group -> {
sortOrder.removeListener(fileCountListener); sortOrder.addListener(fileCountListener);
group.fileIds().removeListener(fileCountListener); group.fileIds().removeListener(fileCountListener);
group.seenProperty().removeListener(seenListener); group.seenProperty().removeListener(seenListener);
group.uncatCountProperty().removeListener(fileCountListener); group.uncatCountProperty().removeListener(fileCountListener);
@ -159,7 +157,7 @@ class GroupTreeCell extends TreeCell<TreeNode> {
@Nonnull @Nonnull
private String getSeenStyleClass() { private String getSeenStyleClass() {
return Optional.ofNullable(getItem()) return Optional.ofNullable(getItem())
.map(TreeNode::getGroup) .map(GroupTreeNode::getGroup)
.map(DrawableGroup::isSeen) .map(DrawableGroup::isSeen)
.map(seen -> seen ? "" : "-fx-font-weight:bold;") .map(seen -> seen ? "" : "-fx-font-weight:bold;")
.orElse(""); //if item is null or group is null .orElse(""); //if item is null or group is null
@ -173,13 +171,12 @@ class GroupTreeCell extends TreeCell<TreeNode> {
*/ */
@Nonnull @Nonnull
private String getCountsText() { private String getCountsText() {
return Optional.ofNullable(getItem()) return Optional.ofNullable(getItem())
.filter(treeNode -> treeNode.getGroup() != null) .map(GroupTreeNode::getGroup)
.map(item -> .map(group ->
sortOrder.get() == TreeNodeComparator.ALPHABETICAL " (" + (sortOrder.get() == GroupComparators.ALPHABETICAL
? " (" + item.getSize() + ")" ? group.getSize()
: " (" + sortOrder.get().getFormattedValueOfTreeNode(item) + ")") : sortOrder.get().getFormattedValueOfGroup(group)) + ")"
.orElse(""); //if item is null or group is null ).orElse(""); //if item is null or group is null
} }
} }

View File

@ -38,7 +38,7 @@ import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup;
* {@link GroupTreeCell}. Each GroupTreeItem has a TreeNode which has a path * {@link GroupTreeCell}. Each GroupTreeItem has a TreeNode which has a path
* segment and may or may not have a group * segment and may or may not have a group
*/ */
class GroupTreeItem extends TreeItem<TreeNode> implements Comparable<GroupTreeItem> { class GroupTreeItem extends TreeItem<GroupTreeNode> {
static final Executor treeInsertTread = Executors.newSingleThreadExecutor(); static final Executor treeInsertTread = Executors.newSingleThreadExecutor();
@ -64,12 +64,12 @@ class GroupTreeItem extends TreeItem<TreeNode> implements Comparable<GroupTreeIt
/** /**
* the comparator if any used to sort the children of this item * the comparator if any used to sort the children of this item
*/ */
private Comparator<TreeNode> comp; private Comparator<DrawableGroup> comp;
GroupTreeItem(String t, DrawableGroup g, Comparator<TreeNode> comp, boolean expanded) { GroupTreeItem(String t, DrawableGroup g, boolean expanded) {
super(new TreeNode(t, g)); super(new GroupTreeNode(t, g));
this.comp = comp;
setExpanded(expanded); setExpanded(expanded);
comp = GroupComparators.ALPHABETICAL;
} }
/** /**
@ -92,7 +92,7 @@ class GroupTreeItem extends TreeItem<TreeNode> implements Comparable<GroupTreeIt
* @param g Group to add * @param g Group to add
* @param tree True if it is part of a tree (versus a list) * @param tree True if it is part of a tree (versus a list)
*/ */
synchronized void insert(List<String> path, DrawableGroup g, Boolean tree) { synchronized void insert(List<String> path, DrawableGroup g, boolean tree) {
if (tree) { if (tree) {
// Are we at the end of the recursion? // Are we at the end of the recursion?
if (path.isEmpty()) { if (path.isEmpty()) {
@ -101,7 +101,7 @@ class GroupTreeItem extends TreeItem<TreeNode> implements Comparable<GroupTreeIt
String prefix = path.get(0); String prefix = path.get(0);
GroupTreeItem prefixTreeItem = childMap.computeIfAbsent(prefix, (String t) -> { GroupTreeItem prefixTreeItem = childMap.computeIfAbsent(prefix, (String t) -> {
final GroupTreeItem newTreeItem = new GroupTreeItem(t, null, comp, false); final GroupTreeItem newTreeItem = new GroupTreeItem(t, null, false);
Platform.runLater(() -> { Platform.runLater(() -> {
getChildren().add(newTreeItem); getChildren().add(newTreeItem);
@ -111,7 +111,7 @@ class GroupTreeItem extends TreeItem<TreeNode> implements Comparable<GroupTreeIt
// recursively go into the path // recursively go into the path
treeInsertTread.execute(() -> { treeInsertTread.execute(() -> {
prefixTreeItem.insert(path.subList(1, path.size()), g, tree); prefixTreeItem.insert(path.subList(1, path.size()), g, true);
}); });
} }
@ -119,25 +119,16 @@ class GroupTreeItem extends TreeItem<TreeNode> implements Comparable<GroupTreeIt
String join = StringUtils.join(path, "/"); String join = StringUtils.join(path, "/");
//flat list //flat list
childMap.computeIfAbsent(join, (String t) -> { childMap.computeIfAbsent(join, (String t) -> {
final GroupTreeItem newTreeItem = new GroupTreeItem(t, g, comp, false); final GroupTreeItem newTreeItem = new GroupTreeItem(t, g, true);
newTreeItem.setExpanded(true);
Platform.runLater(() -> { Platform.runLater(() -> {
getChildren().add(newTreeItem); getChildren().add(newTreeItem);
if (comp != null) { getChildren().sort(Comparator.comparing(treeItem -> treeItem.getValue().getGroup(), comp));
getChildren().sort(Comparator.comparing(TreeItem::getValue, comp));
}
}); });
return newTreeItem; return newTreeItem;
}); });
} }
} }
@Override
public int compareTo(GroupTreeItem o) {
return comp.compare(this.getValue(), o.getValue());
}
synchronized GroupTreeItem getTreeItemForPath(List<String> path) { synchronized GroupTreeItem getTreeItemForPath(List<String> path) {
if (path.isEmpty()) { if (path.isEmpty()) {
@ -178,9 +169,9 @@ class GroupTreeItem extends TreeItem<TreeNode> implements Comparable<GroupTreeIt
* @param newComp * @param newComp
*/ */
@ThreadConfined(type = ThreadConfined.ThreadType.JFX) @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
synchronized void resortChildren(Comparator<TreeNode> newComp) { synchronized void resortChildren(Comparator<DrawableGroup> newComp) {
this.comp = newComp; this.comp = newComp;
getChildren().sort(Comparator.comparing(TreeItem::getValue, comp)); getChildren().sort(Comparator.comparing(treeItem -> treeItem.getValue().getGroup(), comp));
for (GroupTreeItem ti : childMap.values()) { for (GroupTreeItem ti : childMap.values()) {
ti.resortChildren(comp); ti.resortChildren(comp);
} }

View File

@ -23,7 +23,7 @@ import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup;
/** /**
* *
*/ */
class TreeNode { class GroupTreeNode {
private final String path; private final String path;
private DrawableGroup group; private DrawableGroup group;
@ -36,7 +36,7 @@ class TreeNode {
return group; return group;
} }
TreeNode(String path, DrawableGroup group) { GroupTreeNode(String path, DrawableGroup group) {
this.path = path; this.path = path;
this.group = group; this.group = group;
} }
@ -44,24 +44,4 @@ class TreeNode {
void setGroup(DrawableGroup g) { void setGroup(DrawableGroup g) {
group = g; group = g;
} }
public String getGroupByValueDislpayName() {
return (group == null) ? "" : group.getGroupByValueDislpayName();
}
public synchronized int getSize() {
return (group == null) ? 0 : group.getSize();
}
public double getHashHitDensity() {
return (group == null) ? 0 : group.getHashHitDensity();
}
public synchronized long getHashSetHitsCount() {
return (group == null) ? 0 : group.getHashSetHitsCount();
}
public final synchronized long getUncategorizedCount() {
return (group == null) ? 0 : group.getUncategorizedCount();
}
} }

View File

@ -0,0 +1,96 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2016 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.imagegallery.gui.navpanel;
import java.util.function.Function;
import javafx.collections.transformation.SortedList;
import javafx.fxml.FXML;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionModel;
import javafx.scene.image.ImageView;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.imagegallery.FXMLConstructor;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup;
/**
* Shows only groups with hash hits in a flat list, with controls to adjust
* sorting of list.
*/
final public class HashHitGroupList extends NavPanel<DrawableGroup> {
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private final ListView<DrawableGroup> groupList = new ListView<>();
/**
* sorted list of groups, setting a new comparator on this changes the
* sorting in the ListView.
*/
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private SortedList<DrawableGroup> sorted;
public HashHitGroupList(ImageGalleryController controller) {
super(controller);
FXMLConstructor.construct(this, "NavPanel.fxml");
}
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
@Override
SelectionModel<DrawableGroup> getSelectionModel() {
return groupList.getSelectionModel();
}
@Override
Function< DrawableGroup, DrawableGroup> getDataItemMapper() {
return Function.identity(); // this view already works with groups, so do nothing
}
@Override
void applyGroupComparator() {
sorted.setComparator(getComparator());
}
@FXML
@Override
void initialize() {
super.initialize();
setText("Only Hash Hits");
setGraphic(new ImageView("org/sleuthkit/autopsy/imagegallery/images/hashset_hits.png"));
getBorderPane().setCenter(groupList);
sorted = getController().getGroupManager().getAnalyzedGroups().filtered((DrawableGroup t) -> t.getHashSetHitsCount() > 0).sorted();
groupList.setCellFactory(treeView -> new GroupListCell(getSortByBox().getSelectionModel().selectedItemProperty()));
groupList.setItems(sorted);
}
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
@Override
void setFocusedGroup(DrawableGroup grouping) {
groupList.getSelectionModel().select(grouping);
}
@Override
GroupComparators<Long> getDefaultComparator() {
return GroupComparators.HIT_COUNT;
}
}

View File

@ -4,53 +4,22 @@
<?import javafx.collections.FXCollections?> <?import javafx.collections.FXCollections?>
<?import javafx.scene.control.ComboBox?> <?import javafx.scene.control.ComboBox?>
<?import javafx.scene.control.Label?> <?import javafx.scene.control.Label?>
<?import javafx.scene.control.RadioButton?>
<?import javafx.scene.control.Tab?> <?import javafx.scene.control.Tab?>
<?import javafx.scene.control.TabPane?> <?import javafx.scene.control.RadioButton?>
<?import javafx.scene.control.ToggleGroup?> <?import javafx.scene.control.ToggleGroup?>
<?import javafx.scene.control.ToolBar?> <?import javafx.scene.control.ToolBar?>
<?import javafx.scene.control.TreeView?>
<?import javafx.scene.image.Image?> <?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?> <?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.AnchorPane?> <?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.BorderPane?> <?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.VBox?>
<fx:root maxHeight="1.7976931348623157E308" type="VBox" xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1">
<children> <fx:root type="Tab" xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1" closable="false" >
<TabPane fx:id="navTabPane" maxHeight="1.7976931348623157E308" prefHeight="-1.0" prefWidth="-1.0" tabClosingPolicy="UNAVAILABLE" VBox.vgrow="ALWAYS">
<tabs>
<Tab fx:id="navTab" text="All Groups">
<graphic>
<ImageView fitHeight="16.0" fitWidth="16.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../../images/folder-tree.png" />
</image>
</ImageView>
</graphic>
<content> <content>
<AnchorPane fx:id="navAnchor"> <BorderPane prefHeight="200.0" prefWidth="200.0" fx:id="borderPane">
<children>
<TreeView fx:id="navTree" maxHeight="1.7976931348623157E308" minWidth="-1.0" prefHeight="-1.0" prefWidth="-1.0" style="" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" />
</children>
</AnchorPane>
</content>
</Tab>
<Tab fx:id="hashTab" text="Groups With Hash Hits">
<graphic>
<ImageView fitHeight="16.0" fitWidth="16.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../../images/hashset_hits.png" />
</image>
</ImageView>
</graphic>
<content>
<BorderPane fx:id="treeBorder">
<center>
<AnchorPane fx:id="hashAnchor" />
</center>
<top> <top>
<ToolBar BorderPane.alignment="CENTER"> <ToolBar fx:id="toolBar" BorderPane.alignment="CENTER">
<items> <items>
<Label text="Sort By:" /> <Label text="Sort By:" />
<ComboBox fx:id="sortByBox" maxWidth="-Infinity" minWidth="-Infinity"> <ComboBox fx:id="sortByBox" maxWidth="-Infinity" minWidth="-Infinity">
@ -92,8 +61,4 @@
</top> </top>
</BorderPane> </BorderPane>
</content> </content>
</Tab>
</tabs>
</TabPane>
</children>
</fx:root> </fx:root>

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2013-16 Basis Technology Corp. * Copyright 2016 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org * Contact: carrier <at> sleuthkit <dot> org
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -19,63 +19,37 @@
package org.sleuthkit.autopsy.imagegallery.gui.navpanel; package org.sleuthkit.autopsy.imagegallery.gui.navpanel;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import javafx.application.Platform; import java.util.function.Function;
import javafx.collections.ListChangeListener;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.ComboBox; import javafx.scene.control.ComboBox;
import javafx.scene.control.RadioButton; import javafx.scene.control.RadioButton;
import javafx.scene.control.SelectionMode; import javafx.scene.control.SelectionModel;
import javafx.scene.control.Tab; import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.ToggleGroup; import javafx.scene.control.ToggleGroup;
import javafx.scene.control.TreeItem; import javafx.scene.control.ToolBar;
import javafx.scene.control.TreeView; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.apache.commons.lang3.StringUtils;
import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.coreutils.ThreadConfined.ThreadType;
import org.sleuthkit.autopsy.imagegallery.FXMLConstructor;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager; import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState;
/** /**
* Display two trees. one shows all folders (groups) and calls out folders with * Base class for Tabs in the left hand Navigation/Context area.
* images. the user can select folders with images to see them in the main
* GroupPane The other shows folders with hash set hits.
*/ */
final public class NavPanel extends VBox { abstract class NavPanel<X> extends Tab {
@FXML @FXML
private ComboBox<TreeNodeComparator<?>> sortByBox; private BorderPane borderPane;
@FXML @FXML
private TabPane navTabPane; private ToolBar toolBar;
@FXML @FXML
private TreeView<TreeNode> navTree; private ComboBox<GroupComparators<?>> sortByBox;
@FXML
private AnchorPane hashAnchor;
@FXML
private AnchorPane navAnchor;
@FXML
private Tab hashTab;
@FXML
private Tab navTab;
private final GroupTreeItem navTreeRoot = new GroupTreeItem("", null, TreeNodeComparator.ALPHABETICAL, true);
private final GroupTreeItem hashTreeRoot = new GroupTreeItem("", null, TreeNodeComparator.HIT_COUNT, true);
@FXML @FXML
private RadioButton ascRadio; private RadioButton ascRadio;
@ -87,161 +61,155 @@ final public class NavPanel extends VBox {
private RadioButton descRadio; private RadioButton descRadio;
private final ImageGalleryController controller; private final ImageGalleryController controller;
private final GroupManager groupManager;
private final CategoryManager categoryManager;
private TreeNodeComparator<?> hashSortOrder = TreeNodeComparator.HIT_COUNT; NavPanel(ImageGalleryController controller) {
public NavPanel(ImageGalleryController controller) {
this.controller = controller; this.controller = controller;
FXMLConstructor.construct(this, "NavPanel.fxml"); this.groupManager = controller.getGroupManager();
} this.categoryManager = controller.getCategoryManager();
@Subscribe
public void handleCategoryChange(CategoryManager.CategoryChangeEvent event) {
resortHashTree();
} }
@FXML @FXML
void initialize() { void initialize() {
assert hashTab != null : "fx:id=\"hashTab\" was not injected: check your FXML file 'NavPanel.fxml'."; assert borderPane != null : "fx:id=\"borderPane\" was not injected: check your FXML file 'NavPanel.fxml'.";
assert navTab != null : "fx:id=\"navTab\" was not injected: check your FXML file 'NavPanel.fxml'."; assert toolBar != null : "fx:id=\"toolBar\" was not injected: check your FXML file 'NavPanel.fxml'.";
assert navTabPane != null : "fx:id=\"navTabPane\" was not injected: check your FXML file 'NavPanel.fxml'.";
assert navTree != null : "fx:id=\"navTree\" was not injected: check your FXML file 'NavPanel.fxml'.";
assert sortByBox != null : "fx:id=\"sortByBox\" was not injected: check your FXML file 'NavPanel.fxml'."; assert sortByBox != null : "fx:id=\"sortByBox\" was not injected: check your FXML file 'NavPanel.fxml'.";
assert ascRadio != null : "fx:id=\"ascRadio\" was not injected: check your FXML file 'NavPanel.fxml'.";
assert orderGroup != null : "fx:id=\"orderGroup\" was not injected: check your FXML file 'NavPanel.fxml'.";
assert descRadio != null : "fx:id=\"descRadio\" was not injected: check your FXML file 'NavPanel.fxml'.";
VBox.setVgrow(this, Priority.ALWAYS); sortByBox.getItems().setAll(GroupComparators.getValues());
sortByBox.getSelectionModel().select(getDefaultComparator());
sortByBox.getItems().setAll(TreeNodeComparator.getValues()); orderGroup.selectedToggleProperty().addListener(order -> sortGroups());
sortByBox.getSelectionModel().select(TreeNodeComparator.ALPHABETICAL);
sortByBox.getSelectionModel().selectedItemProperty().addListener(observable -> { sortByBox.getSelectionModel().selectedItemProperty().addListener(observable -> {
sortGroups();
if (sortByBox.getSelectionModel().getSelectedItem() == TreeNodeComparator.UNCATEGORIZED_COUNT) { //only need to listen to changes in category if we are sorting by/ showing the uncategorized count
controller.getCategoryManager().registerListener(NavPanel.this); if (sortByBox.getSelectionModel().getSelectedItem() == GroupComparators.UNCATEGORIZED_COUNT) {
categoryManager.registerListener(NavPanel.this);
} else { } else {
controller.getCategoryManager().unregisterListener(NavPanel.this); categoryManager.unregisterListener(NavPanel.this);
} }
resortHashTree();
});
orderGroup.selectedToggleProperty().addListener(observable -> resortHashTree());
navTree.setCellFactory(treeView -> new GroupTreeCell(sortByBox.getSelectionModel().selectedItemProperty()));
navTree.setShowRoot(false);
navTree.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
navTree.getSelectionModel().selectedItemProperty().addListener(o -> updateControllersGroup());
navTree.setRoot(navTreeRoot);
navTabPane.getSelectionModel().selectedItemProperty().addListener(observable -> {
Tab selectedTab = navTabPane.getSelectionModel().getSelectedItem();
if (selectedTab == navTab) {
hashSortOrder = sortByBox.getSelectionModel().getSelectedItem();
sortByBox.getSelectionModel().select(TreeNodeComparator.ALPHABETICAL);
navTree.setRoot(navTreeRoot);
hashAnchor.getChildren().clear();
navAnchor.getChildren().add(navTree);
} else if (selectedTab == hashTab) {
sortByBox.getSelectionModel().select(hashSortOrder);
navTree.setRoot(hashTreeRoot);
navAnchor.getChildren().clear();
hashAnchor.getChildren().add(navTree);
resortHashTree();
}
});
controller.getGroupManager().getAnalyzedGroups().addListener((ListChangeListener.Change<? extends DrawableGroup> change) -> {
while (change.next()) {
change.getAddedSubList().stream().forEach(this::insertGroup);
change.getRemoved().stream().forEach(this::removeFromTree);
}
}); });
//keep selection in sync with controller
controller.viewState().addListener(observable -> { controller.viewState().addListener(observable -> {
Optional.ofNullable(controller.viewState().get()) Optional.ofNullable(controller.viewState().get())
.map(GroupViewState::getGroup) .map(GroupViewState::getGroup)
.ifPresent(this::setFocusedGroup); .ifPresent(this::setFocusedGroup);
}); });
for (DrawableGroup g : controller.getGroupManager().getAnalyzedGroups()) { getSelectionModel().selectedItemProperty().addListener(o -> updateControllersGroup());
insertGroup(g);
}
} }
private void insertGroup(DrawableGroup g) { /**
navTreeRoot.insert(groupingToPath(g), g, true); * @return the default comparator used by this "view" to sort groups
if (g.getHashSetHitsCount() > 0) { */
hashTreeRoot.insert(groupingToPath(g), g, false); abstract GroupComparators<?> getDefaultComparator();
}
@Subscribe
public void handleCategoryChange(CategoryManager.CategoryChangeEvent event) {
sortGroups();
} }
private void updateControllersGroup() { /**
Optional.ofNullable(navTree.getSelectionModel().getSelectedItem()) * @return the a comparator that will enforce the currently selected sorting
.map(TreeItem::getValue) * options.
.map(TreeNode::getGroup) */
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
Comparator<DrawableGroup> getComparator() {
Comparator<DrawableGroup> comparator = sortByBox.getSelectionModel().getSelectedItem();
return (orderGroup.getSelectedToggle() == ascRadio)
? comparator
: comparator.reversed();
}
/**
* notify controller about group selection in this view
*/
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
void updateControllersGroup() {
Optional.ofNullable(getSelectionModel().getSelectedItem())
.map(getDataItemMapper())
.ifPresent(group -> controller.advance(GroupViewState.tile(group), false)); .ifPresent(group -> controller.advance(GroupViewState.tile(group), false));
} }
@ThreadConfined(type = ThreadType.JFX) /**
private void resortHashTree() { * Sort the groups in this view according to the currently selected sorting
Comparator<TreeNode> treeNodeComparator = sortByBox.getSelectionModel().getSelectedItem(); * options. Attempts to maintain selection.
if (orderGroup.getSelectedToggle() == descRadio) { */
treeNodeComparator = treeNodeComparator.reversed(); @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
} void sortGroups() {
TreeItem<TreeNode> selectedItem = navTree.getSelectionModel().getSelectedItem(); X selectedItem = getSelectionModel().getSelectedItem();
hashTreeRoot.resortChildren(treeNodeComparator); applyGroupComparator();
navTree.getSelectionModel().select(selectedItem); Optional.ofNullable(selectedItem)
.map(getDataItemMapper())
.ifPresent(this::setFocusedGroup);
} }
/** /**
* Set the tree to the passed in group * @return a function that maps the "native" data type of this view to a
* * DrawableGroup
* @param grouping
*/ */
@ThreadConfined(type = ThreadType.JFX) abstract Function<X, DrawableGroup> getDataItemMapper();
private void setFocusedGroup(DrawableGroup grouping) {
final GroupTreeItem treeItemForGroup = ((GroupTreeItem) navTree.getRoot()).getTreeItemForPath(groupingToPath(grouping));
if (treeItemForGroup != null) { /**
navTree.getSelectionModel().select(treeItemForGroup); * Apply the currently selected sorting options.
Platform.runLater(() -> { */
int row = navTree.getRow(treeItemForGroup); abstract void applyGroupComparator();
if (row != -1) {
navTree.scrollTo(row - 2); //put newly selected row 3 from the top
}
});
}
}
private List<String> groupingToPath(DrawableGroup g) {
String path = g.groupKey.getValueDisplayName();
if (g.groupKey.getAttribute() != DrawableAttribute.PATH
|| navTabPane.getSelectionModel().getSelectedItem() == hashTab) {
String stripStart = StringUtils.strip(path, "/");
return Arrays.asList(stripStart);
} else {
String[] cleanPathTokens = StringUtils.stripStart(path, "/").split("/");
return Arrays.asList(cleanPathTokens);
}
}
/** /**
* *
* @param g the value of g * @return get the selection model
* @param treeRoot the value of treeRoot
*/ */
private void removeFromTree(DrawableGroup g) { @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
GroupTreeItem treeItemForGroup = navTreeRoot.getTreeItemForGroup(g); abstract SelectionModel<X> getSelectionModel();
if (treeItemForGroup != null) {
treeItemForGroup.removeFromParent(); /**
* attempt to set the given group as the selected/focused group in this
* view.
*
* @param grouping the grouping to attempt to select
*/
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
abstract void setFocusedGroup(DrawableGroup grouping);
////boring getters
BorderPane getBorderPane() {
return borderPane;
} }
treeItemForGroup = hashTreeRoot.getTreeItemForGroup(g); ToolBar getToolBar() {
if (treeItemForGroup != null) { return toolBar;
treeItemForGroup.removeFromParent();
}
} }
public void showTree() { ComboBox<GroupComparators<?>> getSortByBox() {
Platform.runLater(() -> { return sortByBox;
navTabPane.getSelectionModel().select(navTab);
});
} }
RadioButton getAscRadio() {
return ascRadio;
}
ToggleGroup getOrderGroup() {
return orderGroup;
}
RadioButton getDescRadio() {
return descRadio;
}
ImageGalleryController getController() {
return controller;
}
GroupManager getGroupManager() {
return groupManager;
}
CategoryManager getCategoryManager() {
return categoryManager;
}
} }

View File

@ -1,82 +0,0 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2013-16 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.imagegallery.gui.navpanel;
import com.google.common.collect.ImmutableList;
import java.util.Comparator;
import java.util.function.Function;
/**
*
*/
final class TreeNodeComparator<T extends Comparable<T>> implements Comparator<TreeNode> {
static final TreeNodeComparator<Long> UNCATEGORIZED_COUNT =
new TreeNodeComparator<>("Uncategorized Count", TreeNode::getUncategorizedCount, String::valueOf, false);
static final TreeNodeComparator<String> ALPHABETICAL =
new TreeNodeComparator<>("Group Name", TreeNode::getGroupByValueDislpayName, String::valueOf, false);
static final TreeNodeComparator<Long> HIT_COUNT =
new TreeNodeComparator<>("Hit Count", TreeNode::getHashSetHitsCount, String::valueOf, true);
static final TreeNodeComparator<Integer> FILE_COUNT =
new TreeNodeComparator<>("Group Size", TreeNode::getSize, String::valueOf, true);
static final TreeNodeComparator<Double> HIT_FILE_RATIO =
new TreeNodeComparator<>("Hit Density", (treeNode) -> treeNode.getHashHitDensity(), density -> String.format("%.2f", density), true);
private final static ImmutableList<TreeNodeComparator<?>> values = ImmutableList.of(UNCATEGORIZED_COUNT, ALPHABETICAL, HIT_COUNT, FILE_COUNT, HIT_FILE_RATIO);
public static ImmutableList<TreeNodeComparator<?>> getValues() {
return values;
}
private final Function<TreeNode, T> extractor;
private final Function<T, String> valueFormatter;
private final boolean orderReveresed;
private final String displayName;
private TreeNodeComparator(String displayName, Function<TreeNode, T> extractor, Function<T, String> formatter, boolean defaultOrderReversed) {
this.displayName = displayName;
this.extractor = extractor;
this.orderReveresed = defaultOrderReversed;
this.valueFormatter = formatter;
}
@Override
public int compare(TreeNode o1, TreeNode o2) {
int compareTo = extractor.apply(o1).compareTo(extractor.apply(o2));
return orderReveresed ? -compareTo : compareTo;
}
public String getDisplayName() {
return displayName;
}
@Override
public String toString() {
return displayName;
}
String getFormattedValueOfTreeNode(TreeNode group) {
return valueFormatter.apply(extractor.apply(group));
}
}