Merge branch 'develop' of https://github.com/sleuthkit/autopsy into 4167-UpdateCommonAttributeGUI

This commit is contained in:
William Schaefer 2018-09-17 10:30:26 -04:00
commit a2a0970e48
51 changed files with 2890 additions and 2112 deletions

View File

@ -329,6 +329,7 @@
<package>org.sleuthkit.autopsy.guiutils</package> <package>org.sleuthkit.autopsy.guiutils</package>
<package>org.sleuthkit.autopsy.healthmonitor</package> <package>org.sleuthkit.autopsy.healthmonitor</package>
<package>org.sleuthkit.autopsy.ingest</package> <package>org.sleuthkit.autopsy.ingest</package>
<package>org.sleuthkit.autopsy.ingest.events</package>
<package>org.sleuthkit.autopsy.keywordsearchservice</package> <package>org.sleuthkit.autopsy.keywordsearchservice</package>
<package>org.sleuthkit.autopsy.menuactions</package> <package>org.sleuthkit.autopsy.menuactions</package>
<package>org.sleuthkit.autopsy.modules.encryptiondetection</package> <package>org.sleuthkit.autopsy.modules.encryptiondetection</package>

View File

@ -413,7 +413,7 @@ public class ImageUtils {
String cacheDirectory = Case.getCurrentCaseThrows().getCacheDirectory(); String cacheDirectory = Case.getCurrentCaseThrows().getCacheDirectory();
return Paths.get(cacheDirectory, "thumbnails", fileID + ".png").toFile(); //NON-NLS return Paths.get(cacheDirectory, "thumbnails", fileID + ".png").toFile(); //NON-NLS
} catch (NoCurrentCaseException e) { } catch (NoCurrentCaseException e) {
LOGGER.log(Level.WARNING, "Could not get cached thumbnail location. No case is open."); //NON-NLS LOGGER.log(Level.INFO, "Could not get cached thumbnail location. No case is open."); //NON-NLS
return null; return null;
} }
}); });

View File

@ -23,11 +23,13 @@ import org.openide.util.NbBundle;
import static org.sleuthkit.autopsy.directorytree.Bundle.*; import static org.sleuthkit.autopsy.directorytree.Bundle.*;
@NbBundle.Messages({"SelectionContext.dataSources=Data Sources", @NbBundle.Messages({"SelectionContext.dataSources=Data Sources",
"SelectionContext.dataSourceFiles=Data Source Files",
"SelectionContext.views=Views"}) "SelectionContext.views=Views"})
enum SelectionContext { enum SelectionContext {
DATA_SOURCES(SelectionContext_dataSources()), DATA_SOURCES(SelectionContext_dataSources()),
VIEWS(SelectionContext_views()), VIEWS(SelectionContext_views()),
OTHER(""); // Subnode of another node. OTHER(""), // Subnode of another node.
DATA_SOURCE_FILES(SelectionContext_dataSourceFiles());
private final String displayName; private final String displayName;
@ -36,7 +38,7 @@ enum SelectionContext {
} }
public static SelectionContext getContextFromName(String name) { public static SelectionContext getContextFromName(String name) {
if (name.equals(DATA_SOURCES.getName())) { if (name.equals(DATA_SOURCES.getName()) || name.equals(DATA_SOURCE_FILES.getName())) {
return DATA_SOURCES; return DATA_SOURCES;
} else if (name.equals(VIEWS.getName())) { } else if (name.equals(VIEWS.getName())) {
return VIEWS; return VIEWS;
@ -64,6 +66,16 @@ enum SelectionContext {
// One level below root node. Should be one of DataSources, Views, or Results // One level below root node. Should be one of DataSources, Views, or Results
return SelectionContext.getContextFromName(n.getDisplayName()); return SelectionContext.getContextFromName(n.getDisplayName());
} else { } else {
// In Group by Data Source mode, the node under root is the data source name, and
// under that is Data Source Files, Views, or Results. Before moving up the tree, check
// if one of those applies.
if (n.getParentNode().getParentNode().getParentNode() == null) {
SelectionContext context = SelectionContext.getContextFromName(n.getDisplayName());
if (context != SelectionContext.OTHER) {
return context;
}
}
return getSelectionContext(n.getParentNode()); return getSelectionContext(n.getParentNode());
} }
} }

View File

@ -26,7 +26,7 @@ import org.sleuthkit.datamodel.Content;
* Event published when analysis (ingest) of a data source included in an ingest * Event published when analysis (ingest) of a data source included in an ingest
* job is completed. * job is completed.
*/ */
public class DataSourceAnalysisCompletedEvent extends DataSourceAnalysisEvent implements Serializable { public final class DataSourceAnalysisCompletedEvent extends DataSourceAnalysisEvent implements Serializable {
/** /**
* The reason why the analysis of the data source completed. * The reason why the analysis of the data source completed.

View File

@ -26,7 +26,7 @@ import org.sleuthkit.datamodel.Content;
* Event published when analysis (ingest) of a data source included in an ingest * Event published when analysis (ingest) of a data source included in an ingest
* job is started. * job is started.
*/ */
public class DataSourceAnalysisStartedEvent extends DataSourceAnalysisEvent implements Serializable { public final class DataSourceAnalysisStartedEvent extends DataSourceAnalysisEvent implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2015-17 Basis Technology Corp. * Copyright 2015-18 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");
@ -56,9 +56,7 @@ public final class PromptDialogManager {
@NbBundle.Messages("PrompDialogManager.buttonType.update=Update DB") @NbBundle.Messages("PrompDialogManager.buttonType.update=Update DB")
private static final ButtonType UPDATE = new ButtonType(Bundle.PrompDialogManager_buttonType_update(), ButtonBar.ButtonData.OK_DONE); private static final ButtonType UPDATE = new ButtonType(Bundle.PrompDialogManager_buttonType_update(), ButtonBar.ButtonData.OK_DONE);
/** /** Image to use as title bar icon in dialogs */
* Image to use as title bar icon in dialogs
*/
private static final Image AUTOPSY_ICON; private static final Image AUTOPSY_ICON;
static { static {

View File

@ -230,6 +230,7 @@
<package>org.apache.commons.codec.digest</package> <package>org.apache.commons.codec.digest</package>
<package>org.apache.commons.codec.language</package> <package>org.apache.commons.codec.language</package>
<package>org.apache.commons.codec.net</package> <package>org.apache.commons.codec.net</package>
<package>org.apache.commons.collections4</package>
<package>org.apache.commons.csv</package> <package>org.apache.commons.csv</package>
<package>org.apache.commons.io</package> <package>org.apache.commons.io</package>
<package>org.apache.commons.io.comparator</package> <package>org.apache.commons.io.comparator</package>

View File

@ -21,21 +21,24 @@ package org.sleuthkit.autopsy.imagegallery;
import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener; import java.beans.PropertyChangeListener;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.logging.Level; import java.util.logging.Level;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.Observable; import javafx.beans.Observable;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.ReadOnlyIntegerProperty; import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper; import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.beans.property.ReadOnlyLongWrapper;
import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
@ -43,25 +46,14 @@ import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.concurrent.Worker; import javafx.concurrent.Worker;
import javafx.geometry.Insets; import javax.annotation.Nonnull;
import javafx.scene.Node; import static org.apache.commons.collections4.CollectionUtils.isNotEmpty;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javax.annotation.Nullable;
import javax.swing.SwingUtilities;
import org.netbeans.api.progress.ProgressHandle; import org.netbeans.api.progress.ProgressHandle;
import org.openide.util.Cancellable; import org.openide.util.Cancellable;
import org.openide.util.NbBundle; import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.Case.CaseType;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent;
import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent;
import org.sleuthkit.autopsy.core.RuntimeProperties;
import org.sleuthkit.autopsy.coreutils.History; import org.sleuthkit.autopsy.coreutils.History;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
@ -69,18 +61,18 @@ import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.imagegallery.actions.UndoRedoManager; import org.sleuthkit.autopsy.imagegallery.actions.UndoRedoManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager; import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableDB; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableDB;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableDB.DrawableDbBuildStatusEnum;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableTagsManager; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableTagsManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.HashSetManager; import org.sleuthkit.autopsy.imagegallery.datamodel.HashSetManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupManager; 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.Toolbar;
import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.ingest.IngestManager;
import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector; import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector;
import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.DataSource;
import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.SleuthkitCase.CaseDbTransaction;
import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.datamodel.TskData; import org.sleuthkit.datamodel.TskData;
@ -90,8 +82,7 @@ import org.sleuthkit.datamodel.TskData;
*/ */
public final class ImageGalleryController { public final class ImageGalleryController {
private static final Logger LOGGER = Logger.getLogger(ImageGalleryController.class.getName()); private static final Logger logger = Logger.getLogger(ImageGalleryController.class.getName());
private static ImageGalleryController instance;
/** /**
* true if Image Gallery should listen to ingest events, false if it should * true if Image Gallery should listen to ingest events, false if it should
@ -103,7 +94,7 @@ public final class ImageGalleryController {
private final ReadOnlyBooleanWrapper stale = new ReadOnlyBooleanWrapper(false); private final ReadOnlyBooleanWrapper stale = new ReadOnlyBooleanWrapper(false);
private final ReadOnlyBooleanWrapper metaDataCollapsed = new ReadOnlyBooleanWrapper(false); private final ReadOnlyBooleanWrapper metaDataCollapsed = new ReadOnlyBooleanWrapper(false);
private final ReadOnlyDoubleWrapper thumbnailSize = new ReadOnlyDoubleWrapper(100); private final SimpleDoubleProperty thumbnailSizeProp = new SimpleDoubleProperty(100);
private final ReadOnlyBooleanWrapper regroupDisabled = new ReadOnlyBooleanWrapper(false); private final ReadOnlyBooleanWrapper regroupDisabled = new ReadOnlyBooleanWrapper(false);
private final ReadOnlyIntegerWrapper dbTaskQueueSize = new ReadOnlyIntegerWrapper(0); private final ReadOnlyIntegerWrapper dbTaskQueueSize = new ReadOnlyIntegerWrapper(0);
@ -111,36 +102,23 @@ public final class ImageGalleryController {
private final History<GroupViewState> historyManager = new History<>(); private final History<GroupViewState> historyManager = new History<>();
private final UndoRedoManager undoManager = new UndoRedoManager(); private final UndoRedoManager undoManager = new UndoRedoManager();
private final GroupManager groupManager = new GroupManager(this); private final ThumbnailCache thumbnailCache = new ThumbnailCache(this);
private final HashSetManager hashSetManager = new HashSetManager(); private final GroupManager groupManager;
private final CategoryManager categoryManager = new CategoryManager(this); private final HashSetManager hashSetManager;
private final DrawableTagsManager tagsManager = new DrawableTagsManager(null); private final CategoryManager categoryManager;
private final DrawableTagsManager tagsManager;
private Runnable showTree;
private Toolbar toolbar;
private StackPane fullUIStackPane;
private StackPane centralStackPane;
private Node infoOverlay;
private final Region infoOverLayBackground = new Region() {
{
setBackground(new Background(new BackgroundFill(Color.GREY, CornerRadii.EMPTY, Insets.EMPTY)));
setOpacity(.4);
}
};
private ListeningExecutorService dbExecutor; private ListeningExecutorService dbExecutor;
private SleuthkitCase sleuthKitCase; private final Case autopsyCase;
private DrawableDB db; private final SleuthkitCase sleuthKitCase;
private final DrawableDB drawableDB;
public static synchronized ImageGalleryController getDefault() { public Case getAutopsyCase() {
if (instance == null) { return autopsyCase;
instance = new ImageGalleryController();
}
return instance;
} }
public ReadOnlyBooleanProperty getMetaDataCollapsed() { public ReadOnlyBooleanProperty metaDataCollapsedProperty() {
return metaDataCollapsed.getReadOnlyProperty(); return metaDataCollapsed.getReadOnlyProperty();
} }
@ -148,19 +126,19 @@ public final class ImageGalleryController {
this.metaDataCollapsed.set(metaDataCollapsed); this.metaDataCollapsed.set(metaDataCollapsed);
} }
public ReadOnlyDoubleProperty thumbnailSizeProperty() { public DoubleProperty thumbnailSizeProperty() {
return thumbnailSize.getReadOnlyProperty(); return thumbnailSizeProp;
} }
private GroupViewState getViewState() { public GroupViewState getViewState() {
return historyManager.getCurrentState(); return historyManager.getCurrentState();
} }
public ReadOnlyBooleanProperty regroupDisabled() { public ReadOnlyBooleanProperty regroupDisabledProperty() {
return regroupDisabled.getReadOnlyProperty(); return regroupDisabled.getReadOnlyProperty();
} }
public ReadOnlyObjectProperty<GroupViewState> viewState() { public ReadOnlyObjectProperty<GroupViewState> viewStateProperty() {
return historyManager.currentState(); return historyManager.currentState();
} }
@ -172,8 +150,8 @@ public final class ImageGalleryController {
return groupManager; return groupManager;
} }
synchronized public DrawableDB getDatabase() { public DrawableDB getDatabase() {
return db; return drawableDB;
} }
public void setListeningEnabled(boolean enabled) { public void setListeningEnabled(boolean enabled) {
@ -193,14 +171,9 @@ public final class ImageGalleryController {
Platform.runLater(() -> { Platform.runLater(() -> {
stale.set(b); stale.set(b);
}); });
try {
new PerCaseProperties(Case.getCurrentCaseThrows()).setConfigSetting(ImageGalleryModule.getModuleName(), PerCaseProperties.STALE, b.toString());
} catch (NoCurrentCaseException ex) {
Logger.getLogger(ImageGalleryController.class.getName()).log(Level.WARNING, "Exception while getting open case."); //NON-NLS
}
} }
public ReadOnlyBooleanProperty stale() { public ReadOnlyBooleanProperty staleProperty() {
return stale.getReadOnlyProperty(); return stale.getReadOnlyProperty();
} }
@ -209,50 +182,57 @@ public final class ImageGalleryController {
return stale.get(); return stale.get();
} }
private ImageGalleryController() { ImageGalleryController(@Nonnull Case newCase) throws TskCoreException {
listeningEnabled.addListener((observable, oldValue, newValue) -> { this.autopsyCase = Objects.requireNonNull(newCase);
this.sleuthKitCase = newCase.getSleuthkitCase();
setListeningEnabled(ImageGalleryModule.isEnabledforCase(newCase));
groupManager = new GroupManager(this);
this.drawableDB = DrawableDB.getDrawableDB(this);
categoryManager = new CategoryManager(this);
tagsManager = new DrawableTagsManager(this);
tagsManager.registerListener(groupManager);
tagsManager.registerListener(categoryManager);
hashSetManager = new HashSetManager(drawableDB);
setStale(isDataSourcesTableStale());
dbExecutor = getNewDBExecutor();
// listener for the boolean property about when IG is listening / enabled
listeningEnabled.addListener((observable, wasPreviouslyEnabled, isEnabled) -> {
try { try {
//if we just turned on listening and a case is open and that case is not up to date // if we just turned on listening and a single-user case is open and that case is not up to date, then rebuild it
if (newValue && !oldValue && ImageGalleryModule.isDrawableDBStale(Case.getCurrentCaseThrows())) { // For multiuser cases, we defer DB rebuild till the user actually opens Image Gallery
if (isEnabled && !wasPreviouslyEnabled
&& isDataSourcesTableStale()
&& (Case.getCurrentCaseThrows().getCaseType() == CaseType.SINGLE_USER_CASE)) {
//populate the db //populate the db
queueDBTask(new CopyAnalyzedFiles(instance, db, sleuthKitCase)); this.rebuildDB();
} }
} catch (NoCurrentCaseException ex) { } catch (NoCurrentCaseException ex) {
LOGGER.log(Level.WARNING, "Exception while getting open case.", ex); logger.log(Level.WARNING, "Exception while getting open case.", ex);
} }
}); });
groupManager.getAnalyzedGroups().addListener((Observable o) -> { viewStateProperty().addListener((Observable observable) -> {
//analyzed groups is confined to JFX thread
if (Case.isCaseOpen()) {
checkForGroups();
}
});
groupManager.getUnSeenGroups().addListener((Observable observable) -> {
//if there are unseen groups and none being viewed
if (groupManager.getUnSeenGroups().isEmpty() == false && (getViewState() == null || getViewState().getGroup() == null)) {
advance(GroupViewState.tile(groupManager.getUnSeenGroups().get(0)), true);
}
});
viewState().addListener((Observable observable) -> {
//when the viewed group changes, clear the selection and the undo/redo history //when the viewed group changes, clear the selection and the undo/redo history
selectionModel.clearSelection(); selectionModel.clearSelection();
undoManager.clear(); undoManager.clear();
}); });
regroupDisabled.addListener(observable -> checkForGroups());
IngestManager ingestManager = IngestManager.getInstance(); IngestManager ingestManager = IngestManager.getInstance();
PropertyChangeListener ingestEventHandler = PropertyChangeListener ingestEventHandler
propertyChangeEvent -> Platform.runLater(this::updateRegroupDisabled); = propertyChangeEvent -> Platform.runLater(this::updateRegroupDisabled);
ingestManager.addIngestModuleEventListener(ingestEventHandler); ingestManager.addIngestModuleEventListener(ingestEventHandler);
ingestManager.addIngestJobEventListener(ingestEventHandler); ingestManager.addIngestJobEventListener(ingestEventHandler);
dbTaskQueueSize.addListener(obs -> this.updateRegroupDisabled()); dbTaskQueueSize.addListener(obs -> this.updateRegroupDisabled());
} }
public ReadOnlyBooleanProperty getCanAdvance() { public ReadOnlyBooleanProperty getCanAdvance() {
@ -264,10 +244,7 @@ public final class ImageGalleryController {
} }
@ThreadConfined(type = ThreadConfined.ThreadType.ANY) @ThreadConfined(type = ThreadConfined.ThreadType.ANY)
public void advance(GroupViewState newState, boolean forceShowTree) { public void advance(GroupViewState newState) {
if (forceShowTree && showTree != null) {
showTree.run();
}
historyManager.advance(newState); historyManager.advance(newState);
} }
@ -284,131 +261,92 @@ public final class ImageGalleryController {
regroupDisabled.set((dbTaskQueueSize.get() > 0) || IngestManager.getInstance().isIngestRunning()); regroupDisabled.set((dbTaskQueueSize.get() > 0) || IngestManager.getInstance().isIngestRunning());
} }
/**
* Check if there are any fully analyzed groups available from the
* GroupManager and remove blocking progress spinners if there are. If there
* aren't, add a blocking progress spinner with appropriate message.
*/
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
@NbBundle.Messages({"ImageGalleryController.noGroupsDlg.msg1=No groups are fully analyzed; but listening to ingest is disabled. "
+ " No groups will be available until ingest is finished and listening is re-enabled.",
"ImageGalleryController.noGroupsDlg.msg2=No groups are fully analyzed yet, but ingest is still ongoing. Please Wait.",
"ImageGalleryController.noGroupsDlg.msg3=No groups are fully analyzed yet, but image / video data is still being populated. Please Wait.",
"ImageGalleryController.noGroupsDlg.msg4=There are no images/videos available from the added datasources; but listening to ingest is disabled. "
+ " No groups will be available until ingest is finished and listening is re-enabled.",
"ImageGalleryController.noGroupsDlg.msg5=There are no images/videos in the added datasources.",
"ImageGalleryController.noGroupsDlg.msg6=There are no fully analyzed groups to display:"
+ " the current Group By setting resulted in no groups, "
+ "or no groups are fully analyzed but ingest is not running."})
synchronized private void checkForGroups() {
if (groupManager.getAnalyzedGroups().isEmpty()) {
if (IngestManager.getInstance().isIngestRunning()) {
if (listeningEnabled.not().get()) {
replaceNotification(fullUIStackPane,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg1()));
} else {
replaceNotification(fullUIStackPane,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg2(),
new ProgressIndicator()));
}
} else if (dbTaskQueueSize.get() > 0) {
replaceNotification(fullUIStackPane,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg3(),
new ProgressIndicator()));
} else if (db != null && db.countAllFiles() <= 0) { // there are no files in db
if (listeningEnabled.not().get()) {
replaceNotification(fullUIStackPane,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg4()));
} else {
replaceNotification(fullUIStackPane,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg5()));
}
} else if (!groupManager.isRegrouping()) {
replaceNotification(centralStackPane,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg6()));
}
} else {
clearNotification();
}
}
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private void clearNotification() {
//remove the ingest spinner
if (fullUIStackPane != null) {
fullUIStackPane.getChildren().remove(infoOverlay);
}
//remove the ingest spinner
if (centralStackPane != null) {
centralStackPane.getChildren().remove(infoOverlay);
}
}
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private void replaceNotification(StackPane stackPane, Node newNode) {
clearNotification();
infoOverlay = new StackPane(infoOverLayBackground, newNode);
if (stackPane != null) {
stackPane.getChildren().add(infoOverlay);
}
}
/** /**
* configure the controller for a specific case. * configure the controller for a specific case.
* *
* @param theNewCase the case to configure the controller for * @param theNewCase the case to configure the controller for
*
* @throws org.sleuthkit.datamodel.TskCoreException
*/ */
public synchronized void setCase(Case theNewCase) { /**
if (null == theNewCase) { * Rebuilds the DrawableDB database.
reset(); *
} else { */
this.sleuthKitCase = theNewCase.getSleuthkitCase(); public void rebuildDB() {
this.db = DrawableDB.getDrawableDB(ImageGalleryModule.getModuleOutputDir(theNewCase), this); // queue a rebuild task for each stale data source
getStaleDataSourceIds().forEach(dataSourceObjId -> queueDBTask(new CopyAnalyzedFiles(dataSourceObjId, this)));
setListeningEnabled(ImageGalleryModule.isEnabledforCase(theNewCase));
setStale(ImageGalleryModule.isDrawableDBStale(theNewCase));
// if we add this line icons are made as files are analyzed rather than on demand.
// db.addUpdatedFileListener(IconCache.getDefault());
historyManager.clear();
groupManager.setDB(db);
hashSetManager.setDb(db);
categoryManager.setDb(db);
tagsManager.setAutopsyTagsManager(theNewCase.getServices().getTagsManager());
tagsManager.registerListener(groupManager);
tagsManager.registerListener(categoryManager);
shutDownDBExecutor();
dbExecutor = getNewDBExecutor();
}
} }
/** /**
* reset the state of the controller (eg if the case is closed) * reset the state of the controller (eg if the case is closed)
*/ */
public synchronized void reset() { public synchronized void reset() {
LOGGER.info("resetting ImageGalleryControler to initial state."); //NON-NLS logger.info("Closing ImageGalleryControler for case."); //NON-NLS
selectionModel.clearSelection(); selectionModel.clearSelection();
setListeningEnabled(false); thumbnailCache.clearCache();
ThumbnailCache.getDefault().clearCache();
historyManager.clear(); historyManager.clear();
groupManager.clear(); groupManager.reset();
tagsManager.clearFollowUpTagName();
tagsManager.unregisterListener(groupManager);
tagsManager.unregisterListener(categoryManager);
shutDownDBExecutor(); shutDownDBExecutor();
dbExecutor = getNewDBExecutor();
if (toolbar != null) {
toolbar.reset();
} }
if (db != null) { /**
db.closeDBCon(); * Checks if the datasources table in drawable DB is stale.
*
* @return true if datasources table is stale
*/
public boolean isDataSourcesTableStale() {
return isNotEmpty(getStaleDataSourceIds());
} }
db = null;
/**
* Returns a set of data source object ids that are stale.
*
* This includes any data sources already in the table, that are not in
* COMPLETE status, or any data sources that might have been added to the
* case, but are not in the datasources table.
*
* @return list of data source object ids that are stale.
*/
Set<Long> getStaleDataSourceIds() {
Set<Long> staleDataSourceIds = new HashSet<>();
// no current case open to check
if ((null == getDatabase()) || (null == getSleuthKitCase())) {
return staleDataSourceIds;
}
try {
Map<Long, DrawableDbBuildStatusEnum> knownDataSourceIds = getDatabase().getDataSourceDbBuildStatus();
List<DataSource> dataSources = getSleuthKitCase().getDataSources();
Set<Long> caseDataSourceIds = new HashSet<>();
dataSources.stream().map(DataSource::getId).forEach(caseDataSourceIds::add);
// collect all data sources already in the table, that are not yet COMPLETE
knownDataSourceIds.entrySet().stream().forEach((Map.Entry<Long, DrawableDbBuildStatusEnum> t) -> {
DrawableDbBuildStatusEnum status = t.getValue();
if (DrawableDbBuildStatusEnum.COMPLETE != status) {
staleDataSourceIds.add(t.getKey());
}
});
// collect any new data sources in the case.
caseDataSourceIds.forEach((Long id) -> {
if (!knownDataSourceIds.containsKey(id)) {
staleDataSourceIds.add(id);
}
});
return staleDataSourceIds;
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Image Gallery failed to check if datasources table is stale.", ex);
return staleDataSourceIds;
}
} }
synchronized private void shutDownDBExecutor() { synchronized private void shutDownDBExecutor() {
@ -417,7 +355,7 @@ public final class ImageGalleryController {
try { try {
dbExecutor.awaitTermination(30, TimeUnit.SECONDS); dbExecutor.awaitTermination(30, TimeUnit.SECONDS);
} catch (InterruptedException ex) { } catch (InterruptedException ex) {
LOGGER.log(Level.WARNING, "Image Gallery failed to shutdown DB Task Executor in a timely fashion.", ex); logger.log(Level.WARNING, "Image Gallery failed to shutdown DB Task Executor in a timely fashion.", ex);
} }
} }
} }
@ -449,46 +387,14 @@ public final class ImageGalleryController {
Platform.runLater(() -> dbTaskQueueSize.set(dbTaskQueueSize.get() - 1)); Platform.runLater(() -> dbTaskQueueSize.set(dbTaskQueueSize.get() - 1));
} }
@Nullable public DrawableFile getFileFromID(Long fileID) throws TskCoreException {
synchronized public DrawableFile getFileFromId(Long fileID) throws TskCoreException { return drawableDB.getFileFromID(fileID);
if (Objects.isNull(db)) {
LOGGER.log(Level.WARNING, "Could not get file from id, no DB set. The case is probably closed."); //NON-NLS
return null;
}
return db.getFileFromID(fileID);
}
public void setStacks(StackPane fullUIStack, StackPane centralStack) {
fullUIStackPane = fullUIStack;
this.centralStackPane = centralStack;
Platform.runLater(this::checkForGroups);
}
public synchronized void setToolbar(Toolbar toolbar) {
if (this.toolbar != null) {
throw new IllegalStateException("Can not set the toolbar a second time!");
}
this.toolbar = toolbar;
thumbnailSize.bind(toolbar.thumbnailSizeProperty());
} }
public ReadOnlyDoubleProperty regroupProgress() { public ReadOnlyDoubleProperty regroupProgress() {
return groupManager.regroupProgress(); return groupManager.regroupProgress();
} }
/**
* invoked by {@link OnStart} to make sure that the ImageGallery listeners
* get setup as early as possible, and do other setup stuff.
*/
void onStart() {
Platform.setImplicitExit(false);
LOGGER.info("setting up ImageGallery listeners"); //NON-NLS
//TODO can we do anything usefull in an InjestJobEventListener?
//IngestManager.getInstance().addIngestJobEventListener((PropertyChangeEvent evt) -> {});
IngestManager.getInstance().addIngestModuleEventListener(new IngestModuleEventListener());
Case.addPropertyChangeListener(new CaseEventListener());
}
public HashSetManager getHashSetManager() { public HashSetManager getHashSetManager() {
return hashSetManager; return hashSetManager;
} }
@ -501,10 +407,6 @@ public final class ImageGalleryController {
return tagsManager; return tagsManager;
} }
public void setShowTree(Runnable showTree) {
this.showTree = showTree;
}
public UndoRedoManager getUndoManager() { public UndoRedoManager getUndoManager() {
return undoManager; return undoManager;
} }
@ -515,6 +417,12 @@ public final class ImageGalleryController {
public synchronized SleuthkitCase getSleuthKitCase() { public synchronized SleuthkitCase getSleuthKitCase() {
return sleuthKitCase; return sleuthKitCase;
}
public ThumbnailCache getThumbsCache() {
return thumbnailCache;
} }
/** /**
@ -594,7 +502,7 @@ public final class ImageGalleryController {
return file; return file;
} }
public FileTask(AbstractFile f, DrawableDB taskDB) { FileTask(AbstractFile f, DrawableDB taskDB) {
super(); super();
this.file = f; this.file = f;
this.taskDB = taskDB; this.taskDB = taskDB;
@ -604,7 +512,7 @@ public final class ImageGalleryController {
/** /**
* task that updates one file in database with results from ingest * task that updates one file in database with results from ingest
*/ */
static private class UpdateFileTask extends FileTask { static class UpdateFileTask extends FileTask {
UpdateFileTask(AbstractFile f, DrawableDB taskDB) { UpdateFileTask(AbstractFile f, DrawableDB taskDB) {
super(f, taskDB); super(f, taskDB);
@ -631,7 +539,7 @@ public final class ImageGalleryController {
/** /**
* task that updates one file in database with results from ingest * task that updates one file in database with results from ingest
*/ */
static private class RemoveFileTask extends FileTask { static class RemoveFileTask extends FileTask {
RemoveFileTask(AbstractFile f, DrawableDB taskDB) { RemoveFileTask(AbstractFile f, DrawableDB taskDB) {
super(f, taskDB); super(f, taskDB);
@ -654,51 +562,74 @@ public final class ImageGalleryController {
} }
} }
/**
* Base abstract class for various methods of copying image files data, for
* a given data source, into the Image gallery DB.
*/
@NbBundle.Messages({"BulkTask.committingDb.status=committing image/video database", @NbBundle.Messages({"BulkTask.committingDb.status=committing image/video database",
"BulkTask.stopCopy.status=Stopping copy to drawable db task.", "BulkTask.stopCopy.status=Stopping copy to drawable db task.",
"BulkTask.errPopulating.errMsg=There was an error populating Image Gallery database."}) "BulkTask.errPopulating.errMsg=There was an error populating Image Gallery database."})
/* Base abstract class for various methods of copying data into the Image gallery DB */ abstract static class BulkTransferTask extends BackgroundTask {
abstract static private class BulkTransferTask extends BackgroundTask {
static private final String FILE_EXTENSION_CLAUSE = static private final String FILE_EXTENSION_CLAUSE
"(name LIKE '%." //NON-NLS = "(extension LIKE '" //NON-NLS
+ String.join("' OR name LIKE '%.", FileTypeUtils.getAllSupportedExtensions()) //NON-NLS + String.join("' OR extension LIKE '", FileTypeUtils.getAllSupportedExtensions()) //NON-NLS
+ "') "; + "') ";
static private final String MIMETYPE_CLAUSE = static private final String MIMETYPE_CLAUSE
"(mime_type LIKE '" //NON-NLS = "(mime_type LIKE '" //NON-NLS
+ String.join("' OR mime_type LIKE '", FileTypeUtils.getAllSupportedMimeTypes()) //NON-NLS + String.join("' OR mime_type LIKE '", FileTypeUtils.getAllSupportedMimeTypes()) //NON-NLS
+ "') "; + "') ";
static final String DRAWABLE_QUERY = private final String DRAWABLE_QUERY;
//grab files with supported extension private final String DATASOURCE_CLAUSE;
"(" + FILE_EXTENSION_CLAUSE
protected final ImageGalleryController controller;
protected final DrawableDB taskDB;
protected final SleuthkitCase tskCase;
protected final long dataSourceObjId;
private ProgressHandle progressHandle;
private boolean taskCompletionStatus;
BulkTransferTask(long dataSourceObjId, ImageGalleryController controller) {
this.controller = controller;
this.taskDB = controller.getDatabase();
this.tskCase = controller.getSleuthKitCase();
this.dataSourceObjId = dataSourceObjId;
DATASOURCE_CLAUSE = " (data_source_obj_id = " + dataSourceObjId + ") ";
DRAWABLE_QUERY
= DATASOURCE_CLAUSE
+ " AND ( "
+ //grab files with supported extension
FILE_EXTENSION_CLAUSE
//grab files with supported mime-types //grab files with supported mime-types
+ " OR " + MIMETYPE_CLAUSE //NON-NLS + " OR " + MIMETYPE_CLAUSE //NON-NLS
//grab files with image or video mime-types even if we don't officially support them //grab files with image or video mime-types even if we don't officially support them
+ " OR mime_type LIKE 'video/%' OR mime_type LIKE 'image/%' )"; //NON-NLS + " OR mime_type LIKE 'video/%' OR mime_type LIKE 'image/%' )"; //NON-NLS
final ImageGalleryController controller;
final DrawableDB taskDB;
final SleuthkitCase tskCase;
ProgressHandle progressHandle;
BulkTransferTask(ImageGalleryController controller, DrawableDB taskDB, SleuthkitCase tskCase) {
this.controller = controller;
this.taskDB = taskDB;
this.tskCase = tskCase;
} }
/** /**
* Do any cleanup for this task.
* *
* @param success true if the transfer was successful * @param success true if the transfer was successful
*/ */
abstract void cleanup(boolean success); abstract void cleanup(boolean success);
abstract List<AbstractFile> getFiles() throws TskCoreException; abstract void processFile(final AbstractFile f, DrawableDB.DrawableTransaction tr, CaseDbTransaction caseDBTransaction) throws TskCoreException;
abstract void processFile(final AbstractFile f, DrawableDB.DrawableTransaction tr) throws TskCoreException; /**
* Gets a list of files to process.
*
* @return list of files to process
*
* @throws TskCoreException
*/
List<AbstractFile> getFiles() throws TskCoreException {
return tskCase.findAllFilesWhere(DRAWABLE_QUERY);
}
@Override @Override
public void run() { public void run() {
@ -706,24 +637,32 @@ public final class ImageGalleryController {
progressHandle.start(); progressHandle.start();
updateMessage(Bundle.CopyAnalyzedFiles_populatingDb_status()); updateMessage(Bundle.CopyAnalyzedFiles_populatingDb_status());
DrawableDB.DrawableTransaction drawableDbTransaction = null;
CaseDbTransaction caseDbTransaction = null;
try { try {
//grab all files with supported extension or detected mime types //grab all files with supported extension or detected mime types
final List<AbstractFile> files = getFiles(); final List<AbstractFile> files = getFiles();
progressHandle.switchToDeterminate(files.size()); progressHandle.switchToDeterminate(files.size());
taskDB.insertOrUpdateDataSource(dataSourceObjId, DrawableDB.DrawableDbBuildStatusEnum.IN_PROGRESS);
updateProgress(0.0); updateProgress(0.0);
taskCompletionStatus = true;
int workDone = 0;
//do in transaction //do in transaction
DrawableDB.DrawableTransaction tr = taskDB.beginTransaction(); drawableDbTransaction = taskDB.beginTransaction();
int workDone = 0; caseDbTransaction = tskCase.beginTransaction();
for (final AbstractFile f : files) { for (final AbstractFile f : files) {
if (isCancelled() || Thread.interrupted()) { if (isCancelled() || Thread.interrupted()) {
LOGGER.log(Level.WARNING, "Task cancelled: not all contents may be transfered to drawable database."); //NON-NLS logger.log(Level.WARNING, "Task cancelled or interrupted: not all contents may be transfered to drawable database."); //NON-NLS
taskCompletionStatus = false;
progressHandle.finish(); progressHandle.finish();
break; break;
} }
processFile(f, tr); processFile(f, drawableDbTransaction, caseDbTransaction);
workDone++; workDone++;
progressHandle.progress(f.getName(), workDone); progressHandle.progress(f.getName(), workDone);
@ -737,23 +676,42 @@ public final class ImageGalleryController {
updateProgress(1.0); updateProgress(1.0);
progressHandle.start(); progressHandle.start();
taskDB.commitTransaction(tr, true); caseDbTransaction.commit();
taskDB.commitTransaction(drawableDbTransaction, true);
} catch (TskCoreException ex) { } catch (TskCoreException ex) {
if (null != drawableDbTransaction) {
taskDB.rollbackTransaction(drawableDbTransaction);
}
if (null != caseDbTransaction) {
try {
caseDbTransaction.rollback();
} catch (TskCoreException ex2) {
logger.log(Level.SEVERE, "Error in trying to rollback transaction", ex2); //NON-NLS
}
}
progressHandle.progress(Bundle.BulkTask_stopCopy_status()); progressHandle.progress(Bundle.BulkTask_stopCopy_status());
LOGGER.log(Level.WARNING, "Stopping copy to drawable db task. Failed to transfer all database contents", ex); //NON-NLS logger.log(Level.WARNING, "Stopping copy to drawable db task. Failed to transfer all database contents", ex); //NON-NLS
MessageNotifyUtil.Notify.warn(Bundle.BulkTask_errPopulating_errMsg(), ex.getMessage()); MessageNotifyUtil.Notify.warn(Bundle.BulkTask_errPopulating_errMsg(), ex.getMessage());
cleanup(false); cleanup(false);
return; return;
} finally { } finally {
progressHandle.finish(); progressHandle.finish();
if (taskCompletionStatus) {
taskDB.insertOrUpdateDataSource(dataSourceObjId, DrawableDB.DrawableDbBuildStatusEnum.COMPLETE);
}
updateMessage(""); updateMessage("");
updateProgress(-1.0); updateProgress(-1.0);
} }
cleanup(true); cleanup(taskCompletionStatus);
} }
abstract ProgressHandle getInitialProgressHandle(); abstract ProgressHandle getInitialProgressHandle();
protected void setTaskCompletionStatus(boolean status) {
taskCompletionStatus = status;
}
} }
/** /**
@ -766,24 +724,21 @@ public final class ImageGalleryController {
@NbBundle.Messages({"CopyAnalyzedFiles.committingDb.status=committing image/video database", @NbBundle.Messages({"CopyAnalyzedFiles.committingDb.status=committing image/video database",
"CopyAnalyzedFiles.stopCopy.status=Stopping copy to drawable db task.", "CopyAnalyzedFiles.stopCopy.status=Stopping copy to drawable db task.",
"CopyAnalyzedFiles.errPopulating.errMsg=There was an error populating Image Gallery database."}) "CopyAnalyzedFiles.errPopulating.errMsg=There was an error populating Image Gallery database."})
static private class CopyAnalyzedFiles extends BulkTransferTask { static class CopyAnalyzedFiles extends BulkTransferTask {
CopyAnalyzedFiles(ImageGalleryController controller, DrawableDB taskDB, SleuthkitCase tskCase) { CopyAnalyzedFiles(long dataSourceObjId, ImageGalleryController controller) {
super(controller, taskDB, tskCase); super(dataSourceObjId, controller);
} }
@Override @Override
protected void cleanup(boolean success) { protected void cleanup(boolean success) {
controller.setStale(!success); // at the end of the task, set the stale status based on the
// cumulative status of all data sources
controller.setStale(controller.isDataSourcesTableStale());
} }
@Override @Override
List<AbstractFile> getFiles() throws TskCoreException { void processFile(AbstractFile f, DrawableDB.DrawableTransaction tr, CaseDbTransaction caseDbTransaction) throws TskCoreException {
return tskCase.findAllFilesWhere(DRAWABLE_QUERY);
}
@Override
void processFile(AbstractFile f, DrawableDB.DrawableTransaction tr) {
final boolean known = f.getKnown() == TskData.FileKnown.KNOWN; final boolean known = f.getKnown() == TskData.FileKnown.KNOWN;
if (known) { if (known) {
@ -791,13 +746,21 @@ public final class ImageGalleryController {
} else { } else {
try { try {
if (FileTypeUtils.hasDrawableMIMEType(f)) { //supported mimetype => analyzed //supported mimetype => analyzed
taskDB.updateFile(DrawableFile.create(f, true, false), tr); if (null != f.getMIMEType() && FileTypeUtils.hasDrawableMIMEType(f)) {
} else { //unsupported mimtype => analyzed but shouldn't include taskDB.updateFile(DrawableFile.create(f, true, false), tr, caseDbTransaction);
} else {
// if mimetype of the file hasn't been ascertained, ingest might not have completed yet.
if (null == f.getMIMEType()) {
// set to false to force the DB to be marked as stale
this.setTaskCompletionStatus(false);
} else {
//unsupported mimtype => analyzed but shouldn't include
taskDB.removeFile(f.getId(), tr); taskDB.removeFile(f.getId(), tr);
} }
}
} catch (FileTypeDetector.FileTypeDetectorInitException ex) { } catch (FileTypeDetector.FileTypeDetectorInitException ex) {
throw new RuntimeException(ex); throw new TskCoreException("Failed to initialize FileTypeDetector.", ex);
} }
} }
} }
@ -813,24 +776,17 @@ public final class ImageGalleryController {
* Copy files from a newly added data source into the DB. Get all "drawable" * Copy files from a newly added data source into the DB. Get all "drawable"
* files, based on extension and mime-type. After ingest we use file type id * files, based on extension and mime-type. After ingest we use file type id
* module and if necessary jpeg/png signature matching to add/remove files * module and if necessary jpeg/png signature matching to add/remove files
*
* TODO: create methods to simplify progress value/text updates to both
* netbeans and ImageGallery progress/status
*/ */
@NbBundle.Messages({"PrePopulateDataSourceFiles.committingDb.status=committing image/video database"}) @NbBundle.Messages({"PrePopulateDataSourceFiles.committingDb.status=committing image/video database"})
static private class PrePopulateDataSourceFiles extends BulkTransferTask { static class PrePopulateDataSourceFiles extends BulkTransferTask {
private static final Logger LOGGER = Logger.getLogger(PrePopulateDataSourceFiles.class.getName());
private final Content dataSource;
/** /**
* * @param dataSourceObjId The object ID of the DataSource that is being
* @param dataSourceId Data source object ID * pre-populated into the DrawableDB.
* @param controller The controller for this task.
*/ */
PrePopulateDataSourceFiles(Content dataSource, ImageGalleryController controller, DrawableDB taskDB, SleuthkitCase tskCase) { PrePopulateDataSourceFiles(long dataSourceObjId, ImageGalleryController controller) {
super(controller, taskDB, tskCase); super(dataSourceObjId, controller);
this.dataSource = dataSource;
} }
@Override @Override
@ -838,14 +794,8 @@ public final class ImageGalleryController {
} }
@Override @Override
void processFile(final AbstractFile f, DrawableDB.DrawableTransaction tr) { void processFile(final AbstractFile f, DrawableDB.DrawableTransaction tr, CaseDbTransaction caseDBTransaction) {
taskDB.insertFile(DrawableFile.create(f, false, false), tr); taskDB.insertFile(DrawableFile.create(f, false, false), tr, caseDBTransaction);
}
@Override
List<AbstractFile> getFiles() throws TskCoreException {
long datasourceID = dataSource.getDataSource().getId();
return tskCase.findAllFilesWhere("data_source_obj_id = " + datasourceID + " AND " + DRAWABLE_QUERY);
} }
@Override @Override
@ -854,116 +804,4 @@ public final class ImageGalleryController {
return ProgressHandle.createHandle(Bundle.PrePopulateDataSourceFiles_prepopulatingDb_status(), this); return ProgressHandle.createHandle(Bundle.PrePopulateDataSourceFiles_prepopulatingDb_status(), this);
} }
} }
private class IngestModuleEventListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (RuntimeProperties.runningWithGUI() == false) {
/*
* Running in "headless" mode, no need to process any events.
* This cannot be done earlier because the switch to core
* components inactive may not have been made at start up.
*/
IngestManager.getInstance().removeIngestModuleEventListener(this);
return;
}
switch (IngestManager.IngestModuleEvent.valueOf(evt.getPropertyName())) {
case CONTENT_CHANGED:
//TODO: do we need to do anything here? -jm
case DATA_ADDED:
/*
* we could listen to DATA events and progressivly update
* files, and get data from DataSource ingest modules, but
* given that most modules don't post new artifacts in the
* events and we would have to query for them, without
* knowing which are the new ones, we just ignore these
* events for now. The relevant data should all be captured
* by file done event, anyways -jm
*/
break;
case FILE_DONE:
/**
* getOldValue has fileID getNewValue has
* {@link Abstractfile}
*/
AbstractFile file = (AbstractFile) evt.getNewValue();
if (isListeningEnabled()) {
if (file.isFile()) {
try {
synchronized (ImageGalleryController.this) {
if (ImageGalleryModule.isDrawableAndNotKnown(file)) {
//this file should be included and we don't already know about it from hash sets (NSRL)
queueDBTask(new UpdateFileTask(file, db));
} else if (FileTypeUtils.getAllSupportedExtensions().contains(file.getNameExtension())) {
//doing this check results in fewer tasks queued up, and faster completion of db update
//this file would have gotten scooped up in initial grab, but actually we don't need it
queueDBTask(new RemoveFileTask(file, db));
}
}
} catch (TskCoreException | FileTypeDetector.FileTypeDetectorInitException ex) {
//TODO: What to do here?
LOGGER.log(Level.SEVERE, "Unable to determine if file is drawable and not known. Not making any changes to DB", ex); //NON-NLS
MessageNotifyUtil.Notify.error("Image Gallery Error",
"Unable to determine if file is drawable and not known. Not making any changes to DB. See the logs for details.");
}
}
} else { //TODO: keep track of what we missed for later
setStale(true);
}
break;
}
}
}
private class CaseEventListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (RuntimeProperties.runningWithGUI() == false) {
/*
* Running in "headless" mode, no need to process any events.
* This cannot be done earlier because the switch to core
* components inactive may not have been made at start up.
*/
Case.removePropertyChangeListener(this);
return;
}
switch (Case.Events.valueOf(evt.getPropertyName())) {
case CURRENT_CASE:
Case newCase = (Case) evt.getNewValue();
if (newCase == null) { // case is closing
//close window, reset everything
SwingUtilities.invokeLater(ImageGalleryTopComponent::closeTopComponent);
reset();
} else { // a new case has been opened
setCase(newCase); //connect db, groupmanager, start worker thread
}
break;
case DATA_SOURCE_ADDED:
//copy all file data to drawable databse
Content newDataSource = (Content) evt.getNewValue();
if (isListeningEnabled()) {
queueDBTask(new PrePopulateDataSourceFiles(newDataSource, ImageGalleryController.this, getDatabase(), getSleuthKitCase()));
} else {//TODO: keep track of what we missed for later
setStale(true);
}
break;
case CONTENT_TAG_ADDED:
final ContentTagAddedEvent tagAddedEvent = (ContentTagAddedEvent) evt;
if (getDatabase().isInDB(tagAddedEvent.getAddedTag().getContent().getId())) {
getTagsManager().fireTagAddedEvent(tagAddedEvent);
}
break;
case CONTENT_TAG_DELETED:
final ContentTagDeletedEvent tagDeletedEvent = (ContentTagDeletedEvent) evt;
if (getDatabase().isInDB(tagDeletedEvent.getDeletedTagInfo().getContentID())) {
getTagsManager().fireTagDeletedEvent(tagDeletedEvent);
}
break;
}
}
}
} }

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2013-15 Basis Technology Corp. * Copyright 2013-18 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");
@ -18,32 +18,77 @@
*/ */
package org.sleuthkit.autopsy.imagegallery; package org.sleuthkit.autopsy.imagegallery;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import org.apache.commons.lang3.StringUtils; import java.util.logging.Level;
import javafx.application.Platform;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank;
import org.openide.util.NbBundle; import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent;
import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent;
import org.sleuthkit.autopsy.core.RuntimeProperties;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
import org.sleuthkit.autopsy.events.AutopsyEvent;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableDB; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableDB;
import org.sleuthkit.autopsy.ingest.IngestManager;
import org.sleuthkit.autopsy.ingest.IngestManager.IngestJobEvent;
import static org.sleuthkit.autopsy.ingest.IngestManager.IngestModuleEvent.FILE_DONE;
import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector; import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector;
import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.datamodel.TskData; import org.sleuthkit.datamodel.TskData;
/** static definitions and utilities for the ImageGallery module */ /** static definitions, utilities, and listeners for the ImageGallery module */
@NbBundle.Messages({"ImageGalleryModule.moduleName=Image Gallery"}) @NbBundle.Messages({"ImageGalleryModule.moduleName=Image Gallery"})
public class ImageGalleryModule { public class ImageGalleryModule {
private static final Logger LOGGER = Logger.getLogger(ImageGalleryModule.class.getName()); private static final Logger logger = Logger.getLogger(ImageGalleryModule.class.getName());
private static final String MODULE_NAME = Bundle.ImageGalleryModule_moduleName(); private static final String MODULE_NAME = Bundle.ImageGalleryModule_moduleName();
private static final Object controllerLock = new Object();
private static ImageGalleryController controller;
public static ImageGalleryController getController() throws NoCurrentCaseException {
synchronized (controllerLock) {
if (controller == null) {
try {
controller = new ImageGalleryController(Case.getCurrentCaseThrows());
} catch (NoCurrentCaseException | TskCoreException ex) {
throw new NoCurrentCaseException("Error getting ImageGalleryController for the current case.", ex);
}
}
return controller;
}
}
/**
*
*
* This method is invoked by virtue of the OnStart annotation on the OnStart
* class class
*/
static void onStart() {
Platform.setImplicitExit(false);
logger.info("Setting up ImageGallery listeners"); //NON-NLS
IngestManager.getInstance().addIngestJobEventListener(new IngestJobEventListener());
IngestManager.getInstance().addIngestModuleEventListener(new IngestModuleEventListener());
Case.addPropertyChangeListener(new CaseEventListener());
}
static String getModuleName() { static String getModuleName() {
return MODULE_NAME; return MODULE_NAME;
} }
/** /**
* get the Path to the Case's ImageGallery ModuleOutput subfolder; ie * get the Path to the Case's ImageGallery ModuleOutput subfolder; ie
* ".../[CaseName]/ModuleOutput/Image Gallery/" * ".../[CaseName]/ModuleOutput/Image Gallery/"
@ -53,7 +98,7 @@ public class ImageGalleryModule {
* *
* @return the Path to the ModuleOuput subfolder for Image Gallery * @return the Path to the ModuleOuput subfolder for Image Gallery
*/ */
static Path getModuleOutputDir(Case theCase) { public static Path getModuleOutputDir(Case theCase) {
return Paths.get(theCase.getModuleDirectory(), getModuleName()); return Paths.get(theCase.getModuleDirectory(), getModuleName());
} }
@ -83,18 +128,9 @@ public class ImageGalleryModule {
* @return true if the drawable db is out of date for the given case, false * @return true if the drawable db is out of date for the given case, false
* otherwise * otherwise
*/ */
public static boolean isDrawableDBStale(Case c) { public static boolean isDrawableDBStale(Case c) throws TskCoreException {
if (c != null) { return new ImageGalleryController(c).isDataSourcesTableStale();
String stale = new PerCaseProperties(c).getConfigSetting(ImageGalleryModule.MODULE_NAME, PerCaseProperties.STALE);
return StringUtils.isNotBlank(stale) ? Boolean.valueOf(stale) : true;
} else {
return false;
} }
}
/** /**
* Is the given file 'supported' and not 'known'(nsrl hash hit). If so we * Is the given file 'supported' and not 'known'(nsrl hash hit). If so we
@ -105,7 +141,187 @@ public class ImageGalleryModule {
* @return true if the given {@link AbstractFile} is "drawable" and not * @return true if the given {@link AbstractFile} is "drawable" and not
* 'known', else false * 'known', else false
*/ */
public static boolean isDrawableAndNotKnown(AbstractFile abstractFile) throws TskCoreException, FileTypeDetector.FileTypeDetectorInitException { public static boolean isDrawableAndNotKnown(AbstractFile abstractFile) throws FileTypeDetector.FileTypeDetectorInitException {
return (abstractFile.getKnown() != TskData.FileKnown.KNOWN) && FileTypeUtils.isDrawable(abstractFile); return (abstractFile.getKnown() != TskData.FileKnown.KNOWN) && FileTypeUtils.isDrawable(abstractFile);
} }
/**
* Listener for IngestModuleEvents
*/
static private class IngestModuleEventListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (RuntimeProperties.runningWithGUI() == false) {
/*
* Running in "headless" mode, no need to process any events.
* This cannot be done earlier because the switch to core
* components inactive may not have been made at start up.
*/
IngestManager.getInstance().removeIngestModuleEventListener(this);
return;
}
if (IngestManager.IngestModuleEvent.valueOf(evt.getPropertyName()) != FILE_DONE) {
return;
}
// getOldValue has fileID getNewValue has Abstractfile
AbstractFile file = (AbstractFile) evt.getNewValue();
if (false == file.isFile()) {
return;
}
/* only process individual files in realtime on the node that is
* running the ingest. on a remote node, image files are processed
* enblock when ingest is complete */
if (((AutopsyEvent) evt).getSourceType() != AutopsyEvent.SourceType.LOCAL) {
return;
}
try {
ImageGalleryController con = getController();
if (con.isListeningEnabled()) {
try {
if (isDrawableAndNotKnown(file)) {
//this file should be included and we don't already know about it from hash sets (NSRL)
con.queueDBTask(new ImageGalleryController.UpdateFileTask(file, controller.getDatabase()));
} else if (FileTypeUtils.getAllSupportedExtensions().contains(file.getNameExtension())) {
/* Doing this check results in fewer tasks queued
* up, and faster completion of db update. This file
* would have gotten scooped up in initial grab, but
* actually we don't need it */
con.queueDBTask(new ImageGalleryController.RemoveFileTask(file, controller.getDatabase()));
}
} catch (FileTypeDetector.FileTypeDetectorInitException ex) {
logger.log(Level.SEVERE, "Unable to determine if file is drawable and not known. Not making any changes to DB", ex); //NON-NLS
MessageNotifyUtil.Notify.error("Image Gallery Error",
"Unable to determine if file is drawable and not known. Not making any changes to DB. See the logs for details.");
}
}
} catch (NoCurrentCaseException ex) {
logger.log(Level.SEVERE, "Attempted to access ImageGallery with no case open.", ex); //NON-NLS
}
}
}
/**
* Listener for case events.
*/
static private class CaseEventListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (RuntimeProperties.runningWithGUI() == false) {
/*
* Running in "headless" mode, no need to process any events.
* This cannot be done earlier because the switch to core
* components inactive may not have been made at start up.
*/
Case.removePropertyChangeListener(this);
return;
}
ImageGalleryController con;
try {
con = getController();
} catch (NoCurrentCaseException ex) {
logger.log(Level.SEVERE, "Attempted to access ImageGallery with no case open.", ex); //NON-NLS
return;
}
switch (Case.Events.valueOf(evt.getPropertyName())) {
case CURRENT_CASE:
synchronized (controllerLock) {
// case has changes: close window, reset everything
SwingUtilities.invokeLater(ImageGalleryTopComponent::closeTopComponent);
if (controller != null) {
controller.reset();
}
controller = null;
Case newCase = (Case) evt.getNewValue();
if (newCase != null) {
// a new case has been opened: connect db, groupmanager, start worker thread
try {
controller = new ImageGalleryController(newCase);
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Error changing case in ImageGallery.", ex);
}
}
}
break;
case DATA_SOURCE_ADDED:
//For a data source added on the local node, prepopulate all file data to drawable database
if (((AutopsyEvent) evt).getSourceType() == AutopsyEvent.SourceType.LOCAL) {
Content newDataSource = (Content) evt.getNewValue();
if (con.isListeningEnabled()) {
con.queueDBTask(new ImageGalleryController.PrePopulateDataSourceFiles(newDataSource.getId(), controller));
}
}
break;
case CONTENT_TAG_ADDED:
final ContentTagAddedEvent tagAddedEvent = (ContentTagAddedEvent) evt;
if (con.getDatabase().isInDB(tagAddedEvent.getAddedTag().getContent().getId())) {
con.getTagsManager().fireTagAddedEvent(tagAddedEvent);
}
break;
case CONTENT_TAG_DELETED:
final ContentTagDeletedEvent tagDeletedEvent = (ContentTagDeletedEvent) evt;
if (con.getDatabase().isInDB(tagDeletedEvent.getDeletedTagInfo().getContentID())) {
con.getTagsManager().fireTagDeletedEvent(tagDeletedEvent);
}
break;
default:
//we don't need to do anything for other events.
break;
}
}
}
/**
* Listener for Ingest Job events.
*/
static private class IngestJobEventListener implements PropertyChangeListener {
@NbBundle.Messages({
"ImageGalleryController.dataSourceAnalyzed.confDlg.msg= A new data source was added and finished ingest.\n"
+ "The image / video database may be out of date. "
+ "Do you want to update the database with ingest results?\n",
"ImageGalleryController.dataSourceAnalyzed.confDlg.title=Image Gallery"
})
@Override
public void propertyChange(PropertyChangeEvent evt) {
IngestJobEvent eventType = IngestJobEvent.valueOf(evt.getPropertyName());
if (eventType != IngestJobEvent.DATA_SOURCE_ANALYSIS_COMPLETED
|| ((AutopsyEvent) evt).getSourceType() != AutopsyEvent.SourceType.REMOTE) {
return;
}
// A remote node added a new data source and just finished ingest on it.
//drawable db is stale, and if ImageGallery is open, ask user what to do
ImageGalleryController con;
try {
con = getController();
} catch (NoCurrentCaseException ex) {
logger.log(Level.SEVERE, "Attempted to access ImageGallery with no case open.", ex); //NON-NLS
return;
}
con.setStale(true);
if (con.isListeningEnabled() && ImageGalleryTopComponent.isImageGalleryOpen()) {
SwingUtilities.invokeLater(() -> {
int showAnswer = JOptionPane.showConfirmDialog(ImageGalleryTopComponent.getTopComponent(),
Bundle.ImageGalleryController_dataSourceAnalyzed_confDlg_msg(),
Bundle.ImageGalleryController_dataSourceAnalyzed_confDlg_title(),
JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE);
switch (showAnswer) {
case JOptionPane.YES_OPTION:
con.rebuildDB();
break;
case JOptionPane.NO_OPTION:
case JOptionPane.CANCEL_OPTION:
default:
break; //do nothing
}
});
}
}
}
} }

View File

@ -18,13 +18,10 @@
*/ */
package org.sleuthkit.autopsy.imagegallery; package org.sleuthkit.autopsy.imagegallery;
import java.awt.event.ActionEvent;
import java.util.logging.Level;
import org.openide.util.NbBundle; import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.ingest.IngestManager;
import org.sleuthkit.autopsy.coreutils.Logger;
/** /**
* The Image/Video Gallery panel in the NetBeans provided Options Dialogs * The Image/Video Gallery panel in the NetBeans provided Options Dialogs
@ -45,13 +42,8 @@ final class ImageGalleryOptionsPanel extends javax.swing.JPanel {
enabledForCaseBox.setEnabled(Case.isCaseOpen() && IngestManager.getInstance().isIngestRunning() == false); enabledForCaseBox.setEnabled(Case.isCaseOpen() && IngestManager.getInstance().isIngestRunning() == false);
}); });
enabledByDefaultBox.addActionListener((ActionEvent e) -> { enabledByDefaultBox.addActionListener(actionEvent -> controller.changed());
controller.changed(); enabledForCaseBox.addActionListener(actionEvent -> controller.changed());
});
enabledForCaseBox.addActionListener((ActionEvent e) -> {
controller.changed();
});
} }
/** /**
@ -204,19 +196,18 @@ final class ImageGalleryOptionsPanel extends javax.swing.JPanel {
void store() { void store() {
ImageGalleryPreferences.setEnabledByDefault(enabledByDefaultBox.isSelected()); ImageGalleryPreferences.setEnabledByDefault(enabledByDefaultBox.isSelected());
ImageGalleryController.getDefault().setListeningEnabled(enabledForCaseBox.isSelected());
ImageGalleryPreferences.setGroupCategorizationWarningDisabled(groupCategorizationWarningBox.isSelected()); ImageGalleryPreferences.setGroupCategorizationWarningDisabled(groupCategorizationWarningBox.isSelected());
// If a case is open, save the per case setting // If a case is open, save the per case setting
try { try {
Case openCase = Case.getCurrentCaseThrows(); Case openCase = Case.getCurrentCaseThrows();
ImageGalleryModule.getController().setListeningEnabled(enabledForCaseBox.isSelected());
new PerCaseProperties(openCase).setConfigSetting(ImageGalleryModule.getModuleName(), PerCaseProperties.ENABLED, Boolean.toString(enabledForCaseBox.isSelected())); new PerCaseProperties(openCase).setConfigSetting(ImageGalleryModule.getModuleName(), PerCaseProperties.ENABLED, Boolean.toString(enabledForCaseBox.isSelected()));
} catch (NoCurrentCaseException ex) { } catch (NoCurrentCaseException ex) {
// It's not an error if there's no case open // It's not an error if there's no case open
} }
} }
/** /**

View File

@ -46,8 +46,7 @@ public class ImageGalleryPreferences {
* @return true if new cases should have image analyzer enabled. * @return true if new cases should have image analyzer enabled.
*/ */
public static boolean isEnabledByDefault() { public static boolean isEnabledByDefault() {
final boolean aBoolean = preferences.getBoolean(ENABLED_BY_DEFAULT, true); return preferences.getBoolean(ENABLED_BY_DEFAULT, true);
return aBoolean;
} }
public static void setEnabledByDefault(boolean b) { public static void setEnabledByDefault(boolean b) {

View File

@ -18,27 +18,52 @@
*/ */
package org.sleuthkit.autopsy.imagegallery; package org.sleuthkit.autopsy.imagegallery;
import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.embed.swing.JFXPanel; import javafx.embed.swing.JFXPanel;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.control.ChoiceDialog;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.SplitPane; import javafx.scene.control.SplitPane;
import javafx.scene.control.TabPane; import javafx.scene.control.TabPane;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Modality;
import javax.swing.SwingUtilities;
import static org.apache.commons.collections4.CollectionUtils.isNotEmpty;
import static org.apache.commons.lang3.ObjectUtils.notEqual;
import org.openide.explorer.ExplorerManager; import org.openide.explorer.ExplorerManager;
import org.openide.explorer.ExplorerUtils; import org.openide.explorer.ExplorerUtils;
import org.openide.util.Lookup; import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.NbBundle.Messages; import org.openide.util.NbBundle.Messages;
import org.openide.windows.Mode; import org.openide.windows.Mode;
import org.openide.windows.RetainLocation; import org.openide.windows.RetainLocation;
import org.openide.windows.TopComponent; import org.openide.windows.TopComponent;
import org.openide.windows.WindowManager; import org.openide.windows.WindowManager;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupManager;
import org.sleuthkit.autopsy.imagegallery.gui.GuiUtils;
import org.sleuthkit.autopsy.imagegallery.gui.NoGroupsDialog;
import org.sleuthkit.autopsy.imagegallery.gui.StatusBar; import org.sleuthkit.autopsy.imagegallery.gui.StatusBar;
import org.sleuthkit.autopsy.imagegallery.gui.SummaryTablePane; import org.sleuthkit.autopsy.imagegallery.gui.SummaryTablePane;
import org.sleuthkit.autopsy.imagegallery.gui.Toolbar; import org.sleuthkit.autopsy.imagegallery.gui.Toolbar;
@ -46,6 +71,9 @@ 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.GroupTree; import org.sleuthkit.autopsy.imagegallery.gui.navpanel.GroupTree;
import org.sleuthkit.autopsy.imagegallery.gui.navpanel.HashHitGroupList; import org.sleuthkit.autopsy.imagegallery.gui.navpanel.HashHitGroupList;
import org.sleuthkit.autopsy.ingest.IngestManager;
import org.sleuthkit.datamodel.DataSource;
import org.sleuthkit.datamodel.TskCoreException;
/** /**
* Top component which displays ImageGallery interface. * Top component which displays ImageGallery interface.
@ -69,17 +97,17 @@ import org.sleuthkit.autopsy.imagegallery.gui.navpanel.HashHitGroupList;
public final class ImageGalleryTopComponent extends TopComponent implements ExplorerManager.Provider, Lookup.Provider { public final class ImageGalleryTopComponent extends TopComponent implements ExplorerManager.Provider, Lookup.Provider {
public final static String PREFERRED_ID = "ImageGalleryTopComponent"; // NON-NLS public final static String PREFERRED_ID = "ImageGalleryTopComponent"; // NON-NLS
private static final Logger LOGGER = Logger.getLogger(ImageGalleryTopComponent.class.getName()); private static final Logger logger = Logger.getLogger(ImageGalleryTopComponent.class.getName());
private static volatile boolean topComponentInitialized = false; private static volatile boolean topComponentInitialized = false;
private final ExplorerManager em = new ExplorerManager(); private final ExplorerManager em = new ExplorerManager();
private final Lookup lookup = (ExplorerUtils.createLookup(em, getActionMap())); private final Lookup lookup = (ExplorerUtils.createLookup(em, getActionMap()));
private final ImageGalleryController controller = ImageGalleryController.getDefault(); private ImageGalleryController controller;
private SplitPane splitPane; private SplitPane splitPane;
private StackPane centralStack; private StackPane centralStack;
private BorderPane borderPane = new BorderPane(); private final BorderPane borderPane = new BorderPane();
private StackPane fullUIStack; private StackPane fullUIStack;
private MetaDataPane metaDataTable; private MetaDataPane metaDataTable;
private GroupPane groupPane; private GroupPane groupPane;
@ -88,24 +116,97 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl
private VBox leftPane; private VBox leftPane;
private Scene myScene; private Scene myScene;
public static void openTopComponent() { private Node infoOverlay;
//TODO:eventually move to this model, throwing away everything and rebuilding controller groupmanager etc for each case. private final Region infoOverLayBackground = new TranslucentRegion();
// synchronized (OpenTimelineAction.class) {
// if (timeLineController == null) { /**
// timeLineController = new TimeLineController(); * Returns whether the ImageGallery window is open or not.
// LOGGER.log(Level.WARNING, "Failed to get TimeLineController from lookup. Instantiating one directly.S"); *
// } * @return true, if Image gallery is opened, false otherwise
// } */
// timeLineController.openTimeLine(); public static boolean isImageGalleryOpen() {
final TopComponent tc = WindowManager.getDefault().findTopComponent(PREFERRED_ID);
if (tc != null) { final TopComponent topComponent = WindowManager.getDefault().findTopComponent(PREFERRED_ID);
if (topComponent != null) {
return topComponent.isOpened();
}
return false;
}
/**
* Returns the top component window.
*
* @return Image gallery top component window, null if it's not open
*/
public static TopComponent getTopComponent() {
return WindowManager.getDefault().findTopComponent(PREFERRED_ID);
}
@Messages({
"ImageGalleryTopComponent.openTopCommponent.chooseDataSourceDialog.headerText=Choose a data source to view.",
"ImageGalleryTopComponent.openTopCommponent.chooseDataSourceDialog.contentText=Data source:",
"ImageGalleryTopComponent.openTopCommponent.chooseDataSourceDialog.all=All",
"ImageGalleryTopComponent.openTopCommponent.chooseDataSourceDialog.titleText=Image Gallery",})
public static void openTopComponent() throws NoCurrentCaseException {
final TopComponent topComponent = WindowManager.getDefault().findTopComponent(PREFERRED_ID);
if (topComponent == null) {
return;
}
topComponentInitialized = true; topComponentInitialized = true;
if (tc.isOpened() == false) { if (topComponent.isOpened()) {
tc.open(); showTopComponent(topComponent);
return;
} }
tc.toFront();
tc.requestActive(); List<DataSource> dataSources = Collections.emptyList();
ImageGalleryController controller = ImageGalleryModule.getController();
((ImageGalleryTopComponent) topComponent).setController(controller);
try {
dataSources = controller.getSleuthKitCase().getDataSources();
} catch (TskCoreException tskCoreException) {
logger.log(Level.SEVERE, "Unable to get data sourcecs.", tskCoreException);
} }
GroupManager groupManager = controller.getGroupManager();
synchronized (groupManager) {
if (dataSources.size() <= 1
|| groupManager.getGroupBy() != DrawableAttribute.PATH) {
/* if there is only one datasource or the grouping is already
* set to something other than path , don't both to ask for
* datasource */
groupManager.regroup(null, groupManager.getGroupBy(), groupManager.getSortBy(), groupManager.getSortOrder(), true);
showTopComponent(topComponent);
return;
}
}
Map<String, DataSource> dataSourceNames = new HashMap<>();
dataSourceNames.put("All", null);
dataSources.forEach(dataSource -> dataSourceNames.put(dataSource.getName(), dataSource));
Platform.runLater(() -> {
ChoiceDialog<String> datasourceDialog = new ChoiceDialog<>(null, dataSourceNames.keySet());
datasourceDialog.setTitle(Bundle.ImageGalleryTopComponent_openTopCommponent_chooseDataSourceDialog_titleText());
datasourceDialog.setHeaderText(Bundle.ImageGalleryTopComponent_openTopCommponent_chooseDataSourceDialog_headerText());
datasourceDialog.setContentText(Bundle.ImageGalleryTopComponent_openTopCommponent_chooseDataSourceDialog_contentText());
datasourceDialog.initModality(Modality.APPLICATION_MODAL);
GuiUtils.setDialogIcons(datasourceDialog);
Optional<String> dataSourceName = datasourceDialog.showAndWait();
DataSource dataSource = dataSourceName.map(dataSourceNames::get).orElse(null);
synchronized (groupManager) {
groupManager.regroup(dataSource, groupManager.getGroupBy(), groupManager.getSortBy(), groupManager.getSortOrder(), true);
}
SwingUtilities.invokeLater(() -> showTopComponent(topComponent));
});
}
@ThreadConfined(type = ThreadConfined.ThreadType.AWT)
public static void showTopComponent(TopComponent topComponent) {
topComponent.open();
topComponent.toFront();
topComponent.requestActive();
} }
public static void closeTopComponent() { public static void closeTopComponent() {
@ -115,17 +216,27 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl
try { try {
etc.close(); etc.close();
} catch (Exception e) { } catch (Exception e) {
LOGGER.log(Level.SEVERE, "failed to close " + PREFERRED_ID, e); // NON-NLS logger.log(Level.SEVERE, "failed to close " + PREFERRED_ID, e); // NON-NLS
} }
} }
} }
} }
public ImageGalleryTopComponent() { public ImageGalleryTopComponent() throws NoCurrentCaseException {
setName(Bundle.CTL_ImageGalleryTopComponent()); setName(Bundle.CTL_ImageGalleryTopComponent());
initComponents(); initComponents();
setController(ImageGalleryModule.getController());
}
Platform.runLater(() -> {//initialize jfx ui synchronized private void setController(ImageGalleryController controller) {
if (this.controller != null && notEqual(this.controller, controller)) {
this.controller.reset();
}
this.controller = controller;
Platform.runLater(new Runnable() {
@Override
public void run() {
//initialize jfx ui
fullUIStack = new StackPane(); //this is passed into controller fullUIStack = new StackPane(); //this is passed into controller
myScene = new Scene(fullUIStack); myScene = new Scene(fullUIStack);
jfxPanel.setScene(myScene); jfxPanel.setScene(myScene);
@ -137,12 +248,9 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl
Toolbar toolbar = new Toolbar(controller); Toolbar toolbar = new Toolbar(controller);
borderPane.setTop(toolbar); borderPane.setTop(toolbar);
borderPane.setBottom(new StatusBar(controller)); borderPane.setBottom(new StatusBar(controller));
metaDataTable = new MetaDataPane(controller); metaDataTable = new MetaDataPane(controller);
groupTree = new GroupTree(controller); groupTree = new GroupTree(controller);
hashHitList = new HashHitGroupList(controller); hashHitList = new HashHitGroupList(controller);
TabPane tabPane = new TabPane(groupTree, hashHitList); TabPane tabPane = new TabPane(groupTree, hashHitList);
tabPane.setPrefWidth(TabPane.USE_COMPUTED_SIZE); tabPane.setPrefWidth(TabPane.USE_COMPUTED_SIZE);
tabPane.setMinWidth(TabPane.USE_PREF_SIZE); tabPane.setMinWidth(TabPane.USE_PREF_SIZE);
@ -154,9 +262,11 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl
splitPane.getItems().addAll(leftPane, centralStack, metaDataTable); splitPane.getItems().addAll(leftPane, centralStack, metaDataTable);
splitPane.setDividerPositions(0.1, 1.0); splitPane.setDividerPositions(0.1, 1.0);
ImageGalleryController.getDefault().setStacks(fullUIStack, centralStack); controller.regroupDisabledProperty().addListener((Observable observable) -> checkForGroups());
ImageGalleryController.getDefault().setToolbar(toolbar); controller.getGroupManager().getAnalyzedGroups().addListener((Observable observable) -> Platform.runLater(() -> checkForGroups()));
ImageGalleryController.getDefault().setShowTree(() -> tabPane.getSelectionModel().select(groupTree));
Platform.runLater(() -> checkForGroups());
}
}); });
} }
@ -212,4 +322,100 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl
public Lookup getLookup() { public Lookup getLookup() {
return lookup; return lookup;
} }
/**
* Check if there are any fully analyzed groups available from the
* GroupManager and remove blocking progress spinners if there are. If there
* aren't, add a blocking progress spinner with appropriate message.
*/
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
@NbBundle.Messages({
"ImageGalleryController.noGroupsDlg.msg1=No groups are fully analyzed; but listening to ingest is disabled. "
+ " No groups will be available until ingest is finished and listening is re-enabled.",
"ImageGalleryController.noGroupsDlg.msg2=No groups are fully analyzed yet, but ingest is still ongoing. Please Wait.",
"ImageGalleryController.noGroupsDlg.msg3=No groups are fully analyzed yet, but image / video data is still being populated. Please Wait.",
"ImageGalleryController.noGroupsDlg.msg4=There are no images/videos available from the added datasources; but listening to ingest is disabled. "
+ " No groups will be available until ingest is finished and listening is re-enabled.",
"ImageGalleryController.noGroupsDlg.msg5=There are no images/videos in the added datasources.",
"ImageGalleryController.noGroupsDlg.msg6=There are no fully analyzed groups to display:"
+ " the current Group By setting resulted in no groups, "
+ "or no groups are fully analyzed but ingest is not running."})
private void checkForGroups() {
GroupManager groupManager = controller.getGroupManager();
synchronized (groupManager) {
if (isNotEmpty(groupManager.getAnalyzedGroups())) {
clearNotification();
return;
}
if (IngestManager.getInstance().isIngestRunning()) {
if (controller.isListeningEnabled()) {
replaceNotification(centralStack,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg2(),
new ProgressIndicator()));
} else {
replaceNotification(fullUIStack,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg1()));
}
return;
}
if (controller.getDBTasksQueueSizeProperty().get() > 0) {
replaceNotification(fullUIStack,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg3(),
new ProgressIndicator()));
return;
}
try {
if (controller.getDatabase().countAllFiles() <= 0) {
// there are no files in db
if (controller.isListeningEnabled()) {
replaceNotification(fullUIStack,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg5()));
} else {
replaceNotification(fullUIStack,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg4()));
}
return;
}
} catch (TskCoreException tskCoreException) {
logger.log(Level.SEVERE, "Error counting files in the database.", tskCoreException);
}
if (false == groupManager.isRegrouping()) {
replaceNotification(centralStack,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg6()));
}
}
}
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private void replaceNotification(StackPane stackPane, Node newNode) {
clearNotification();
infoOverlay = new StackPane(infoOverLayBackground, newNode);
if (stackPane != null) {
stackPane.getChildren().add(infoOverlay);
}
}
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private void clearNotification() {
//remove the ingest spinner
fullUIStack.getChildren().remove(infoOverlay);
//remove the ingest spinner
centralStack.getChildren().remove(infoOverlay);
}
/**
* Region with partialy opacity used to block out parts of the UI behind a
* pseudo dialog.
*/
static final private class TranslucentRegion extends Region {
TranslucentRegion() {
setBackground(new Background(new BackgroundFill(Color.GREY, CornerRadii.EMPTY, Insets.EMPTY)));
setOpacity(.4);
}
}
} }

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2013 Basis Technology Corp. * Copyright 2013-2018 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");
@ -18,26 +18,19 @@
*/ */
package org.sleuthkit.autopsy.imagegallery; package org.sleuthkit.autopsy.imagegallery;
import org.sleuthkit.autopsy.coreutils.Logger;
/** /**
* * The org.openide.modules.OnStart annotation tells NetBeans to invoke this
* The {@link org.openide.modules.OnStart} annotation tells NetBeans to invoke * class's run method.
* this class's {@link OnStart#run()} method
*/ */
@org.openide.modules.OnStart @org.openide.modules.OnStart
public class OnStart implements Runnable { public class OnStart implements Runnable {
static private final Logger LOGGER = Logger.getLogger(OnStart.class.getName());
/** /**
* * This method is invoked by virtue of the OnStart annotation on the this
* * class
* This method is invoked by virtue of the {@link OnStart} annotation on the
* {@link ImageGalleryModule} class
*/ */
@Override @Override
public void run() { public void run() {
ImageGalleryController.getDefault().onStart(); ImageGalleryModule.onStart();
} }
} }

View File

@ -40,8 +40,6 @@ class PerCaseProperties {
public static final String ENABLED = "enabled"; //NON-NLS public static final String ENABLED = "enabled"; //NON-NLS
public static final String STALE = "stale"; //NON-NLS
private final Case theCase; private final Case theCase;
PerCaseProperties(Case c) { PerCaseProperties(Case c) {

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2013-15 Basis Technology Corp. * Copyright 2013-18 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");
@ -54,9 +54,13 @@ import org.sleuthkit.datamodel.TskCoreException;
* TODO: this was only a singleton for convenience, convert this to * TODO: this was only a singleton for convenience, convert this to
* non-singleton class -jm? * non-singleton class -jm?
*/ */
public enum ThumbnailCache { public class ThumbnailCache {
instance; private final ImageGalleryController controller;
public ThumbnailCache(ImageGalleryController controller) {
this.controller = controller;
}
private static final int MAX_THUMBNAIL_SIZE = 300; private static final int MAX_THUMBNAIL_SIZE = 300;
@ -71,10 +75,6 @@ public enum ThumbnailCache {
.softValues() .softValues()
.expireAfterAccess(10, TimeUnit.MINUTES).build(); .expireAfterAccess(10, TimeUnit.MINUTES).build();
public static ThumbnailCache getDefault() {
return instance;
}
/** /**
* currently desired icon size. is bound in {@link Toolbar} * currently desired icon size. is bound in {@link Toolbar}
*/ */
@ -109,7 +109,7 @@ public enum ThumbnailCache {
@Nullable @Nullable
public Image get(Long fileID) { public Image get(Long fileID) {
try { try {
return get(ImageGalleryController.getDefault().getFileFromId(fileID)); return get(controller.getFileFromID(fileID));
} catch (TskCoreException ex) { } catch (TskCoreException ex) {
LOGGER.log(Level.WARNING, "Failed to load thumbnail for file: " + fileID, ex.getCause()); //NON-NLS LOGGER.log(Level.WARNING, "Failed to load thumbnail for file: " + fileID, ex.getCause()); //NON-NLS
return null; return null;

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2013-2017 Basis Technology Corp. * Copyright 2013-2018 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");
@ -36,7 +36,6 @@ import javax.swing.SwingWorker;
import org.controlsfx.control.action.Action; import org.controlsfx.control.action.Action;
import org.controlsfx.control.action.ActionUtils; import org.controlsfx.control.action.ActionUtils;
import org.openide.util.NbBundle; import org.openide.util.NbBundle;
import org.openide.util.NbBundle.Messages;
import org.openide.windows.TopComponent; import org.openide.windows.TopComponent;
import org.openide.windows.WindowManager; import org.openide.windows.WindowManager;
import org.sleuthkit.autopsy.actions.GetTagNameAndCommentDialog; import org.sleuthkit.autopsy.actions.GetTagNameAndCommentDialog;
@ -50,8 +49,8 @@ import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableTagsManager; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableTagsManager;
import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.ContentTag;
import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.TagName;
import org.sleuthkit.datamodel.TskData;
import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.datamodel.TskData;
/** /**
* Instances of this Action allow users to apply tags to content. * Instances of this Action allow users to apply tags to content.
@ -75,7 +74,7 @@ public class AddTagAction extends Action {
setEventHandler(actionEvent -> addTagWithComment("")); setEventHandler(actionEvent -> addTagWithComment(""));
} }
static public Menu getTagMenu(ImageGalleryController controller) { static public Menu getTagMenu(ImageGalleryController controller) throws TskCoreException {
return new TagMenu(controller); return new TagMenu(controller);
} }
@ -94,7 +93,7 @@ public class AddTagAction extends Action {
DrawableTagsManager tagsManager = controller.getTagsManager(); DrawableTagsManager tagsManager = controller.getTagsManager();
for (Long fileID : selectedFiles) { for (Long fileID : selectedFiles) {
try { try {
final DrawableFile file = controller.getFileFromId(fileID); final DrawableFile file = controller.getFileFromID(fileID);
LOGGER.log(Level.INFO, "tagging {0} with {1} and comment {2}", new Object[]{file.getName(), tagName.getDisplayName(), comment}); //NON-NLS LOGGER.log(Level.INFO, "tagging {0} with {1} and comment {2}", new Object[]{file.getName(), tagName.getDisplayName(), comment}); //NON-NLS
List<ContentTag> contentTags = tagsManager.getContentTags(file); List<ContentTag> contentTags = tagsManager.getContentTags(file);
@ -141,7 +140,7 @@ public class AddTagAction extends Action {
"AddDrawableTagAction.displayName.singular=Tag File"}) "AddDrawableTagAction.displayName.singular=Tag File"})
private static class TagMenu extends Menu { private static class TagMenu extends Menu {
TagMenu(ImageGalleryController controller) { TagMenu(ImageGalleryController controller) throws TskCoreException {
setGraphic(new ImageView(DrawableAttribute.TAGS.getIcon())); setGraphic(new ImageView(DrawableAttribute.TAGS.getIcon()));
ObservableSet<Long> selectedFileIDs = controller.getSelectionModel().getSelected(); ObservableSet<Long> selectedFileIDs = controller.getSelectionModel().getSelected();
setText(selectedFileIDs.size() > 1 setText(selectedFileIDs.size() > 1
@ -163,11 +162,10 @@ public class AddTagAction extends Action {
empty.setDisable(true); empty.setDisable(true);
quickTagMenu.getItems().add(empty); quickTagMenu.getItems().add(empty);
} else { } else {
for (final TagName tagName : tagNames) { tagNames.stream()
AddTagAction addDrawableTagAction = new AddTagAction(controller, tagName, selectedFileIDs); .map(tagName -> new AddTagAction(controller, tagName, selectedFileIDs))
MenuItem tagNameItem = ActionUtils.createMenuItem(addDrawableTagAction); .map(ActionUtils::createMenuItem)
quickTagMenu.getItems().add(tagNameItem); .forEachOrdered(quickTagMenu.getItems()::add);
}
} }
/* /*

View File

@ -138,7 +138,7 @@ public class CategorizeAction extends Action {
TagName catZeroTagName = categoryManager.getTagName(DhsImageCategory.ZERO); TagName catZeroTagName = categoryManager.getTagName(DhsImageCategory.ZERO);
for (long fileID : fileIDs) { for (long fileID : fileIDs) {
try { try {
DrawableFile file = controller.getFileFromId(fileID); //drawable db access DrawableFile file = controller.getFileFromID(fileID); //drawable db access
if (createUndo) { if (createUndo) {
DhsImageCategory oldCat = file.getCategory(); //drawable db access DhsImageCategory oldCat = file.getCategory(); //drawable db access
TagName oldCatTagName = categoryManager.getTagName(oldCat); TagName oldCatTagName = categoryManager.getTagName(oldCat);

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2015-16 Basis Technology Corp. * Copyright 2015-18 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");
@ -34,11 +34,12 @@ import javafx.scene.control.Label;
import javafx.scene.control.Separator; import javafx.scene.control.Separator;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import static org.apache.commons.lang.ObjectUtils.notEqual;
import org.openide.util.NbBundle; import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryPreferences; import org.sleuthkit.autopsy.imagegallery.ImageGalleryPreferences;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskCoreException;
/** /**
@ -52,7 +53,8 @@ public class CategorizeGroupAction extends CategorizeAction {
public CategorizeGroupAction(DhsImageCategory newCat, ImageGalleryController controller) { public CategorizeGroupAction(DhsImageCategory newCat, ImageGalleryController controller) {
super(controller, newCat, null); super(controller, newCat, null);
setEventHandler(actionEvent -> { setEventHandler(actionEvent -> {
ObservableList<Long> fileIDs = controller.viewState().get().getGroup().getFileIDs(); controller.getViewState().getGroup().ifPresent(group -> {
ObservableList<Long> fileIDs = group.getFileIDs();
if (ImageGalleryPreferences.isGroupCategorizationWarningDisabled()) { if (ImageGalleryPreferences.isGroupCategorizationWarningDisabled()) {
//if they have preveiously disabled the warning, just go ahead and apply categories. //if they have preveiously disabled the warning, just go ahead and apply categories.
@ -62,7 +64,7 @@ public class CategorizeGroupAction extends CategorizeAction {
for (Long fileID : fileIDs) { for (Long fileID : fileIDs) {
try { try {
DhsImageCategory category = controller.getFileFromId(fileID).getCategory(); DhsImageCategory category = controller.getFileFromID(fileID).getCategory();
if (false == DhsImageCategory.ZERO.equals(category) && newCat.equals(category) == false) { if (false == DhsImageCategory.ZERO.equals(category) && newCat.equals(category) == false) {
catCountMap.merge(category, 1L, Long::sum); catCountMap.merge(category, 1L, Long::sum);
} }
@ -79,6 +81,8 @@ public class CategorizeGroupAction extends CategorizeAction {
} }
} }
}); });
});
} }
@NbBundle.Messages({"CategorizeGroupAction.OverwriteButton.text=Overwrite", @NbBundle.Messages({"CategorizeGroupAction.OverwriteButton.text=Overwrite",
@ -88,21 +92,20 @@ public class CategorizeGroupAction extends CategorizeAction {
"CategorizeGroupAction.fileCountHeader=Files in the following categories will have their categories overwritten: "}) "CategorizeGroupAction.fileCountHeader=Files in the following categories will have their categories overwritten: "})
private void showConfirmationDialog(final Map<DhsImageCategory, Long> catCountMap, DhsImageCategory newCat, ObservableList<Long> fileIDs) { private void showConfirmationDialog(final Map<DhsImageCategory, Long> catCountMap, DhsImageCategory newCat, ObservableList<Long> fileIDs) {
ButtonType categorizeButtonType = ButtonType categorizeButtonType
new ButtonType(Bundle.CategorizeGroupAction_OverwriteButton_text(), ButtonBar.ButtonData.APPLY); = new ButtonType(Bundle.CategorizeGroupAction_OverwriteButton_text(), ButtonBar.ButtonData.APPLY);
VBox textFlow = new VBox(); VBox textFlow = new VBox();
for (Map.Entry<DhsImageCategory, Long> entry : catCountMap.entrySet()) { for (Map.Entry<DhsImageCategory, Long> entry : catCountMap.entrySet()) {
if (entry.getKey().equals(newCat) == false) { if (entry.getValue() > 0
if (entry.getValue() > 0) { && notEqual(entry.getKey(), newCat)) {
Label label = new Label(Bundle.CategorizeGroupAction_fileCountMessage(entry.getValue(), entry.getKey().getDisplayName()), Label label = new Label(Bundle.CategorizeGroupAction_fileCountMessage(entry.getValue(), entry.getKey().getDisplayName()),
entry.getKey().getGraphic()); entry.getKey().getGraphic());
label.setContentDisplay(ContentDisplay.RIGHT); label.setContentDisplay(ContentDisplay.RIGHT);
textFlow.getChildren().add(label); textFlow.getChildren().add(label);
} }
} }
}
CheckBox checkBox = new CheckBox(Bundle.CategorizeGroupAction_dontShowAgain()); CheckBox checkBox = new CheckBox(Bundle.CategorizeGroupAction_dontShowAgain());
Alert alert = new Alert(Alert.AlertType.CONFIRMATION, "", categorizeButtonType, ButtonType.CANCEL); //NON-NLS Alert alert = new Alert(Alert.AlertType.CONFIRMATION, "", categorizeButtonType, ButtonType.CANCEL); //NON-NLS

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2011-17 Basis Technology Corp. * Copyright 2011-18 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");
@ -18,15 +18,16 @@
*/ */
package org.sleuthkit.autopsy.imagegallery.actions; package org.sleuthkit.autopsy.imagegallery.actions;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.Optional; import java.util.Optional;
import javafx.application.Platform;
import javafx.beans.Observable; import javafx.beans.Observable;
import javafx.beans.binding.ObjectExpression;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import org.apache.commons.collections4.CollectionUtils;
import org.controlsfx.control.action.Action; import org.controlsfx.control.action.Action;
import org.openide.util.NbBundle; import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
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.GroupManager;
@ -36,58 +37,87 @@ import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState;
* Marks the currently displayed group as "seen" and advances to the next unseen * Marks the currently displayed group as "seen" and advances to the next unseen
* group * group
*/ */
@NbBundle.Messages({"NextUnseenGroup.markGroupSeen=Mark Group Seen", @NbBundle.Messages({
"NextUnseenGroup.nextUnseenGroup=Next Unseen group"}) "NextUnseenGroup.markGroupSeen=Mark Group Seen",
"NextUnseenGroup.nextUnseenGroup=Next Unseen group",
"NextUnseenGroup.allGroupsSeen=All Groups Have Been Seen"})
public class NextUnseenGroup extends Action { public class NextUnseenGroup extends Action {
private static final Image END = private static final String IMAGE_PATH = "/org/sleuthkit/autopsy/imagegallery/images/"; //NON-NLS
new Image(NextUnseenGroup.class.getResourceAsStream("/org/sleuthkit/autopsy/imagegallery/images/control-stop.png")); //NON-NLS private static final Image END_IMAGE = new Image(NextUnseenGroup.class.getResourceAsStream(
private static final Image ADVANCE = IMAGE_PATH + "control-stop.png")); //NON-NLS
new Image(NextUnseenGroup.class.getResourceAsStream("/org/sleuthkit/autopsy/imagegallery/images/control-double.png")); //NON-NLS private static final Image ADVANCE_IMAGE = new Image(NextUnseenGroup.class.getResourceAsStream(
IMAGE_PATH + "control-double.png")); //NON-NLS
private static final String MARK_GROUP_SEEN = Bundle.NextUnseenGroup_markGroupSeen(); private static final String MARK_GROUP_SEEN = Bundle.NextUnseenGroup_markGroupSeen();
private static final String NEXT_UNSEEN_GROUP = Bundle.NextUnseenGroup_nextUnseenGroup(); private static final String NEXT_UNSEEN_GROUP = Bundle.NextUnseenGroup_nextUnseenGroup();
private static final String ALL_GROUPS_SEEN = Bundle.NextUnseenGroup_allGroupsSeen();
private final GroupManager groupManager; private final ImageGalleryController controller;
private final ObservableList<DrawableGroup> unSeenGroups; private final ObservableList<DrawableGroup> unSeenGroups;
private final ObservableList<DrawableGroup> analyzedGroups; private final GroupManager groupManager;
public NextUnseenGroup(ImageGalleryController controller) { public NextUnseenGroup(ImageGalleryController controller) {
super(NEXT_UNSEEN_GROUP); super(NEXT_UNSEEN_GROUP);
setGraphic(new ImageView(ADVANCE_IMAGE));
this.controller = controller;
groupManager = controller.getGroupManager(); groupManager = controller.getGroupManager();
unSeenGroups = groupManager.getUnSeenGroups(); unSeenGroups = groupManager.getUnSeenGroups();
analyzedGroups = groupManager.getAnalyzedGroups(); unSeenGroups.addListener((Observable observable) -> updateButton());
setGraphic(new ImageView(ADVANCE)); controller.viewStateProperty().addListener((Observable observable) -> updateButton());
//TODO: do we need both these listeners? setEventHandler(event -> { //on fx-thread
analyzedGroups.addListener((Observable o) -> this.updateButton());
unSeenGroups.addListener((Observable o) -> this.updateButton());
setEventHandler(event -> {
//fx-thread
//if there is a group assigned to the view, mark it as seen //if there is a group assigned to the view, mark it as seen
Optional.ofNullable(controller.viewState()) Optional.ofNullable(controller.getViewState())
.map(ObjectExpression<GroupViewState>::getValue) .flatMap(GroupViewState::getGroup)
.map(GroupViewState::getGroup) .ifPresent(group -> {
.ifPresent(group -> groupManager.markGroupSeen(group, true)); groupManager.markGroupSeen(group, true)
.addListener(this::advanceToNextUnseenGroup, MoreExecutors.newDirectExecutorService());
if (unSeenGroups.isEmpty() == false) { });
controller.advance(GroupViewState.tile(unSeenGroups.get(0)), true);
updateButton();
}
}); });
updateButton(); updateButton();
} }
@ThreadConfined(type = ThreadConfined.ThreadType.JFX) private void advanceToNextUnseenGroup() {
synchronized (groupManager) {
if (CollectionUtils.isNotEmpty(unSeenGroups)) {
controller.advance(GroupViewState.tile(unSeenGroups.get(0)));
}
updateButton();
}
}
private void updateButton() { private void updateButton() {
setDisabled(unSeenGroups.isEmpty()); int size = unSeenGroups.size();
if (unSeenGroups.size() <= 1) {
setText(MARK_GROUP_SEEN); if (size < 1) {
setGraphic(new ImageView(END)); //there are no unseen groups.
Platform.runLater(() -> {
setDisabled(true);
setText(ALL_GROUPS_SEEN);
setGraphic(null);
});
} else { } else {
DrawableGroup get = unSeenGroups.get(0);
DrawableGroup orElse = Optional.ofNullable(controller.getViewState()).flatMap(GroupViewState::getGroup).orElse(null);
boolean equals = get.equals(orElse);
if (size == 1 & equals) {
//The only unseen group is the one that is being viewed.
Platform.runLater(() -> {
setDisabled(false);
setText(MARK_GROUP_SEEN);
setGraphic(new ImageView(END_IMAGE));
});
} else {
//there are more unseen groups.
Platform.runLater(() -> {
setDisabled(false);
setText(NEXT_UNSEEN_GROUP); setText(NEXT_UNSEEN_GROUP);
setGraphic(new ImageView(ADVANCE)); setGraphic(new ImageView(ADVANCE_IMAGE));
});
}
} }
} }
} }

View File

@ -21,16 +21,11 @@ package org.sleuthkit.autopsy.imagegallery.actions;
import java.awt.Component; import java.awt.Component;
import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener; import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.net.URL;
import java.util.logging.Level; import java.util.logging.Level;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType; import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.image.Image;
import javafx.stage.Modality; import javafx.stage.Modality;
import javafx.stage.Stage;
import javax.swing.ImageIcon; import javax.swing.ImageIcon;
import javax.swing.JButton; import javax.swing.JButton;
import javax.swing.JMenuItem; import javax.swing.JMenuItem;
@ -49,42 +44,27 @@ import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.autopsy.core.Installer; import org.sleuthkit.autopsy.core.Installer;
import org.sleuthkit.autopsy.core.RuntimeProperties; import org.sleuthkit.autopsy.core.RuntimeProperties;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryModule; import org.sleuthkit.autopsy.imagegallery.ImageGalleryModule;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryTopComponent; import org.sleuthkit.autopsy.imagegallery.ImageGalleryTopComponent;
import org.sleuthkit.autopsy.imagegallery.gui.GuiUtils;
import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskCoreException;
@ActionID(category = "Tools", id = "org.sleuthkit.autopsy.imagegallery.OpenAction") @ActionID(category = "Tools", id = "org.sleuthkit.autopsy.imagegallery.OpenAction")
@ActionReferences(value = { @ActionReferences(value = {
@ActionReference(path = "Menu/Tools", position = 101), @ActionReference(path = "Menu/Tools", position = 101)
,
@ActionReference(path = "Toolbars/Case", position = 101)}) @ActionReference(path = "Toolbars/Case", position = 101)})
@ActionRegistration(displayName = "#CTL_OpenAction", lazy = false) @ActionRegistration(displayName = "#CTL_OpenAction", lazy = false)
@Messages({"CTL_OpenAction=Images/Videos", @Messages({"CTL_OpenAction=Images/Videos",
"OpenAction.stale.confDlg.msg=The image / video database may be out of date. " + "OpenAction.stale.confDlg.msg=The image / video database may be out of date. "
"Do you want to update and listen for further ingest results?\n" + + "Do you want to update and listen for further ingest results?\n"
"Choosing 'yes' will update the database and enable listening to future ingests.", + "Choosing 'yes' will update the database and enable listening to future ingests.",
"OpenAction.stale.confDlg.title=Image Gallery"}) "OpenAction.stale.confDlg.title=Image Gallery"})
public final class OpenAction extends CallableSystemAction { public final class OpenAction extends CallableSystemAction {
private static final Logger logger = Logger.getLogger(OpenAction.class.getName()); private static final Logger logger = Logger.getLogger(OpenAction.class.getName());
private static final String VIEW_IMAGES_VIDEOS = Bundle.CTL_OpenAction(); private static final String VIEW_IMAGES_VIDEOS = Bundle.CTL_OpenAction();
/**
* Image to use as title bar icon in dialogs
*/
private static final Image AUTOPSY_ICON;
static {
Image tempImg = null;
try {
tempImg = new Image(new URL("nbresloc:/org/netbeans/core/startup/frame.gif").openStream()); //NON-NLS
} catch (IOException ex) {
logger.log(Level.WARNING, "Failed to load branded icon for progress dialog.", ex); //NON-NLS
}
AUTOPSY_ICON = tempImg;
}
private static final long FILE_LIMIT = 6_000_000; private static final long FILE_LIMIT = 6_000_000;
private final PropertyChangeListener pcl; private final PropertyChangeListener pcl;
@ -145,10 +125,7 @@ public final class OpenAction extends CallableSystemAction {
} }
@Override @Override
@SuppressWarnings("fallthrough") @NbBundle.Messages({"OpenAction.dialogTitle=Image Gallery"})
@NbBundle.Messages({
"OpenAction.dialogTitle=Image Gallery"
})
public void performAction() { public void performAction() {
//check case //check case
@ -165,18 +142,40 @@ public final class OpenAction extends CallableSystemAction {
setEnabled(false); setEnabled(false);
return; return;
} }
if (ImageGalleryModule.isDrawableDBStale(currentCase)) { try {
ImageGalleryController controller = ImageGalleryModule.getController();
if (controller.isDataSourcesTableStale()) {
//drawable db is stale, ask what to do //drawable db is stale, ask what to do
int answer = JOptionPane.showConfirmDialog(WindowManager.getDefault().getMainWindow(), Bundle.OpenAction_stale_confDlg_msg(), int answer = JOptionPane.showConfirmDialog(
Bundle.OpenAction_stale_confDlg_title(), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE); WindowManager.getDefault().getMainWindow(),
Bundle.OpenAction_stale_confDlg_msg(),
Bundle.OpenAction_stale_confDlg_title(),
JOptionPane.YES_NO_CANCEL_OPTION,
JOptionPane.WARNING_MESSAGE);
switch (answer) { switch (answer) {
case JOptionPane.YES_OPTION: case JOptionPane.YES_OPTION:
ImageGalleryController.getDefault().setListeningEnabled(true); /* For a single-user case, we favor user experience, and
//fall through * rebuild the database as soon as Image Gallery is
case JOptionPane.NO_OPTION: * enabled for the case. For a multi-user case, we favor
* overall performance and user experience, not every
* user may want to review images, so we rebuild the
* database only when a user launches Image Gallery.
*/
if (currentCase.getCaseType() == Case.CaseType.SINGLE_USER_CASE) {
controller.setListeningEnabled(true);
} else {
controller.rebuildDB();
}
ImageGalleryTopComponent.openTopComponent(); ImageGalleryTopComponent.openTopComponent();
break; break;
case JOptionPane.NO_OPTION: {
ImageGalleryTopComponent.openTopComponent();
}
break;
case JOptionPane.CANCEL_OPTION: case JOptionPane.CANCEL_OPTION:
break; //do nothing break; //do nothing
} }
@ -184,6 +183,9 @@ public final class OpenAction extends CallableSystemAction {
//drawable db is not stale, just open it //drawable db is not stale, just open it
ImageGalleryTopComponent.openTopComponent(); ImageGalleryTopComponent.openTopComponent();
} }
} catch (NoCurrentCaseException noCurrentCaseException) {
logger.log(Level.WARNING, "There was no case open when Image Gallery was opened.", noCurrentCaseException);
}
} }
private boolean tooManyFiles() { private boolean tooManyFiles() {
@ -198,16 +200,6 @@ public final class OpenAction extends CallableSystemAction {
return false; return false;
} }
/**
* Set the title bar icon for the given Dialog to be the Autopsy logo icon.
*
* @param dialog The dialog to set the title bar icon for.
*/
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private static void setDialogIcons(Dialog<?> dialog) {
((Stage) dialog.getDialogPane().getScene().getWindow()).getIcons().setAll(AUTOPSY_ICON);
}
@NbBundle.Messages({ @NbBundle.Messages({
"ImageGallery.showTooManyFiles.contentText=" "ImageGallery.showTooManyFiles.contentText="
+ "There are too many files in the DB to ensure reasonable performance." + "There are too many files in the DB to ensure reasonable performance."
@ -218,7 +210,7 @@ public final class OpenAction extends CallableSystemAction {
Bundle.ImageGallery_showTooManyFiles_contentText(), ButtonType.OK); Bundle.ImageGallery_showTooManyFiles_contentText(), ButtonType.OK);
dialog.initModality(Modality.APPLICATION_MODAL); dialog.initModality(Modality.APPLICATION_MODAL);
dialog.setTitle(Bundle.OpenAction_dialogTitle()); dialog.setTitle(Bundle.OpenAction_dialogTitle());
setDialogIcons(dialog); GuiUtils.setDialogIcons(dialog);
dialog.setHeaderText(Bundle.ImageGallery_showTooManyFiles_headerText()); dialog.setHeaderText(Bundle.ImageGallery_showTooManyFiles_headerText());
dialog.showAndWait(); dialog.showAndWait();
} }

View File

@ -29,9 +29,10 @@ public class TagGroupAction extends AddTagAction {
public TagGroupAction(final TagName tagName, ImageGalleryController controller) { public TagGroupAction(final TagName tagName, ImageGalleryController controller) {
super(controller, tagName, null); super(controller, tagName, null);
setEventHandler(actionEvent -> setEventHandler(actionEvent -> {
new AddTagAction(controller, tagName, ImmutableSet.copyOf(controller.viewState().get().getGroup().getFileIDs())). controller.getViewState().getGroup().ifPresent(group -> {
handle(actionEvent) new AddTagAction(controller, tagName, ImmutableSet.copyOf(group.getFileIDs())).handle(actionEvent);
); });
});
} }
} }

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2015-16 Basis Technology Corp. * Copyright 2015-18 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");
@ -34,20 +34,19 @@ import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent;
import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory; import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.ContentTag;
import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.TagName;
import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskCoreException;
/** /**
* Provides a cached view of the number of files per category, and fires * Provides a cached view of the number of files per category, and fires
* {@link CategoryChangeEvent}s when files are categorized. * CategoryChangeEvents when files are categorized.
* *
* To receive CategoryChangeEvents, a listener must register itself, and * To receive CategoryChangeEvents, a listener must register itself, and
* implement a public method annotated with {@link Subscribe} that accepts one * implement a public method annotated with Subscribe that accepts one argument
* argument of type CategoryChangeEvent * of type CategoryChangeEvent
* *
* TODO: currently these two functions (cached counts and events) are separate * TODO: currently these two functions (cached counts and events) are separate
* although they are related. Can they be integrated more? * although they are related. Can they be integrated more?
@ -64,14 +63,14 @@ public class CategoryManager {
* initialized from this, and the counting of CAT-0 is always delegated to * initialized from this, and the counting of CAT-0 is always delegated to
* this db. * this db.
*/ */
private DrawableDB db; private final DrawableDB drawableDb;
/** /**
* Used to distribute {@link CategoryChangeEvent}s * Used to distribute CategoryChangeEvents
*/ */
private final EventBus categoryEventBus = new AsyncEventBus(Executors.newSingleThreadExecutor( private final EventBus categoryEventBus = new AsyncEventBus(Executors.newSingleThreadExecutor(
new BasicThreadFactory.Builder().namingPattern("Category Event Bus").uncaughtExceptionHandler((Thread t, Throwable e) -> { //NON-NLS new BasicThreadFactory.Builder().namingPattern("Category Event Bus").uncaughtExceptionHandler((Thread thread, Throwable throwable) -> { //NON-NLS
LOGGER.log(Level.SEVERE, "Uncaught exception in category event bus handler", e); //NON-NLS LOGGER.log(Level.SEVERE, "Uncaught exception in category event bus handler", throwable); //NON-NLS
}).build() }).build()
)); ));
@ -80,38 +79,29 @@ public class CategoryManager {
* the count related methods go through this cache, which loads initial * the count related methods go through this cache, which loads initial
* values from the database if needed. * values from the database if needed.
*/ */
private final LoadingCache<DhsImageCategory, LongAdder> categoryCounts = private final LoadingCache<DhsImageCategory, LongAdder> categoryCounts
CacheBuilder.newBuilder().build(CacheLoader.from(this::getCategoryCountHelper)); = CacheBuilder.newBuilder().build(CacheLoader.from(this::getCategoryCountHelper));
/** /**
* cached TagNames corresponding to Categories, looked up from * cached TagNames corresponding to Categories, looked up from
* autopsyTagManager at initial request or if invalidated by case change. * autopsyTagManager at initial request or if invalidated by case change.
*/ */
private final LoadingCache<DhsImageCategory, TagName> catTagNameMap = private final LoadingCache<DhsImageCategory, TagName> catTagNameMap
CacheBuilder.newBuilder().build(CacheLoader.from( = CacheBuilder.newBuilder().build(new CacheLoader<DhsImageCategory, TagName>() {
cat -> getController().getTagsManager().getTagName(cat) @Override
)); public TagName load(DhsImageCategory cat) throws TskCoreException {
return getController().getTagsManager().getTagName(cat);
}
});
public CategoryManager(ImageGalleryController controller) { public CategoryManager(ImageGalleryController controller) {
this.controller = controller; this.controller = controller;
this.drawableDb = controller.getDatabase();
} }
private ImageGalleryController getController() { private ImageGalleryController getController() {
return controller; return controller;
} }
/**
* assign a new db. the counts cache is invalidated and all subsequent db
* lookups go to the new db.
*
* Also clears the Category TagNames
*
* @param db
*/
synchronized public void setDb(DrawableDB db) {
this.db = db;
invalidateCaches();
}
synchronized public void invalidateCaches() { synchronized public void invalidateCaches() {
categoryCounts.invalidateAll(); categoryCounts.invalidateAll();
catTagNameMap.invalidateAll(); catTagNameMap.invalidateAll();
@ -131,7 +121,7 @@ public class CategoryManager {
// is going on, so always use the list of file IDs we already have along with the // is going on, so always use the list of file IDs we already have along with the
// other category counts instead of trying to track it separately. // other category counts instead of trying to track it separately.
long allOtherCatCount = getCategoryCount(DhsImageCategory.ONE) + getCategoryCount(DhsImageCategory.TWO) + getCategoryCount(DhsImageCategory.THREE) + getCategoryCount(DhsImageCategory.FOUR) + getCategoryCount(DhsImageCategory.FIVE); long allOtherCatCount = getCategoryCount(DhsImageCategory.ONE) + getCategoryCount(DhsImageCategory.TWO) + getCategoryCount(DhsImageCategory.THREE) + getCategoryCount(DhsImageCategory.FOUR) + getCategoryCount(DhsImageCategory.FIVE);
return db.getNumberOfImageFilesInList() - allOtherCatCount; return drawableDb.getNumberOfImageFilesInList() - allOtherCatCount;
} else { } else {
return categoryCounts.getUnchecked(cat).sum(); return categoryCounts.getUnchecked(cat).sum();
} }
@ -151,7 +141,7 @@ public class CategoryManager {
/** /**
* decrement the cached value for the number of files with the given * decrement the cached value for the number of files with the given
* {@link DhsImageCategory} * DhsImageCategory
* *
* @param cat the Category to decrement * @param cat the Category to decrement
*/ */
@ -175,7 +165,7 @@ public class CategoryManager {
LongAdder longAdder = new LongAdder(); LongAdder longAdder = new LongAdder();
longAdder.decrement(); longAdder.decrement();
try { try {
longAdder.add(db.getCategoryCount(cat)); longAdder.add(drawableDb.getCategoryCount(cat));
longAdder.increment(); longAdder.increment();
} catch (IllegalStateException ex) { } catch (IllegalStateException ex) {
LOGGER.log(Level.WARNING, "Case closed while getting files"); //NON-NLS LOGGER.log(Level.WARNING, "Case closed while getting files"); //NON-NLS
@ -212,15 +202,14 @@ public class CategoryManager {
try { try {
categoryEventBus.unregister(listener); categoryEventBus.unregister(listener);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
if (e.getMessage().contains("missing event subscriber for an annotated method. Is " + listener + " registered?")) { //NON-NLS
/* /*
* We don't fully understand why we are getting this exception * We don't fully understand why we are getting this exception when
* when the groups should all be registered. To avoid cluttering * the groups should all be registered. To avoid cluttering the logs
* the logs we have disabled recording this exception. This * we have disabled recording this exception. This documented in
* documented in issues 738 and 802. * issues 738 and 802.
*/ */
//LOGGER.log(Level.WARNING, "Attempted to unregister {0} for category change events, but it was not registered.", listener.toString()); //NON-NLS
} else { if (!e.getMessage().contains("missing event subscriber for an annotated method. Is " + listener + " registered?")) { //NON-NLS
throw e; throw e;
} }
} }

View File

@ -18,7 +18,6 @@
*/ */
package org.sleuthkit.autopsy.imagegallery.datamodel; package org.sleuthkit.autopsy.imagegallery.datamodel;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import java.lang.ref.SoftReference; import java.lang.ref.SoftReference;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.ArrayList; import java.util.ArrayList;
@ -32,6 +31,7 @@ import java.util.stream.Collectors;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.util.Pair; import javafx.util.Pair;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
@ -40,8 +40,8 @@ import org.apache.commons.lang3.text.WordUtils;
import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.imagegallery.FileTypeUtils; import org.sleuthkit.autopsy.imagegallery.FileTypeUtils;
import org.sleuthkit.autopsy.imagegallery.ThumbnailCache;
import org.sleuthkit.autopsy.imagegallery.utils.TaskUtils; import org.sleuthkit.autopsy.imagegallery.utils.TaskUtils;
import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardArtifact;
@ -67,15 +67,21 @@ public abstract class DrawableFile {
/** /**
* Skip the database query if we have already determined the file type. * Skip the database query if we have already determined the file type.
*
* @param file The underlying AbstractFile.
* @param analyzed Is the file analyzed.
* @param isVideo Is the file a video.
*
* @return
*/ */
public static DrawableFile create(AbstractFile abstractFileById, boolean analyzed, boolean isVideo) { public static DrawableFile create(AbstractFile file, boolean analyzed, boolean isVideo) {
return isVideo return isVideo
? new VideoFile(abstractFileById, analyzed) ? new VideoFile(file, analyzed)
: new ImageFile(abstractFileById, analyzed); : new ImageFile(file, analyzed);
} }
public static DrawableFile create(Long id, boolean analyzed) throws TskCoreException, NoCurrentCaseException { public static DrawableFile create(Long fileID, boolean analyzed) throws TskCoreException, NoCurrentCaseException {
return create(Case.getCurrentCaseThrows().getSleuthkitCase().getAbstractFileById(id), analyzed); return create(Case.getCurrentCaseThrows().getSleuthkitCase().getAbstractFileById(fileID), analyzed);
} }
private SoftReference<Image> imageRef; private SoftReference<Image> imageRef;
@ -149,8 +155,8 @@ public abstract class DrawableFile {
return file.getSleuthkitCase(); return file.getSleuthkitCase();
} }
private Pair<DrawableAttribute<?>, Collection<?>> makeAttributeValuePair(DrawableAttribute<?> t) { private Pair<DrawableAttribute<?>, Collection<?>> makeAttributeValuePair(DrawableAttribute<?> attribute) {
return new Pair<>(t, t.getValue(DrawableFile.this)); return new Pair<>(attribute, attribute.getValue(this));
} }
public String getModel() { public String getModel() {
@ -254,42 +260,17 @@ public abstract class DrawableFile {
return getSleuthkitCase().getContentTagsByContent(file); return getSleuthkitCase().getContentTagsByContent(file);
} }
@Deprecated
public Image getThumbnail() {
try {
return getThumbnailTask().get();
} catch (InterruptedException | ExecutionException ex) {
return null;
}
}
public Task<Image> getThumbnailTask() {
return ThumbnailCache.getDefault().getThumbnailTask(this);
}
@Deprecated //use non-blocking getReadFullSizeImageTask instead for most cases
public Image getFullSizeImage() {
try {
return getReadFullSizeImageTask().get();
} catch (InterruptedException | ExecutionException ex) {
return null;
}
}
public Task<Image> getReadFullSizeImageTask() { public Task<Image> getReadFullSizeImageTask() {
Image image = (imageRef != null) ? imageRef.get() : null; Image image = (imageRef != null) ? imageRef.get() : null;
if (image == null || image.isError()) { if (image == null || image.isError()) {
Task<Image> readImageTask = getReadFullSizeImageTaskHelper(); Task<Image> readImageTask = getReadFullSizeImageTaskHelper();
readImageTask.stateProperty().addListener(stateProperty -> { readImageTask.stateProperty().addListener(stateProperty -> {
switch (readImageTask.getState()) { if (readImageTask.getState() == Worker.State.SUCCEEDED) {
case SUCCEEDED:
try { try {
imageRef = new SoftReference<>(readImageTask.get()); imageRef = new SoftReference<>(readImageTask.get());
} catch (InterruptedException | ExecutionException exception) { } catch (InterruptedException | ExecutionException exception) {
LOGGER.log(Level.WARNING, getMessageTemplate(exception), getContentPathSafe()); LOGGER.log(Level.WARNING, getMessageTemplate(exception), getContentPathSafe());
} }
break;
} }
}); });
return readImageTask; return readImageTask;

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2013-16 Basis Technology Corp. * Copyright 2013-18 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");
@ -18,25 +18,23 @@
*/ */
package org.sleuthkit.autopsy.imagegallery.datamodel; package org.sleuthkit.autopsy.imagegallery.datamodel;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import com.google.common.eventbus.AsyncEventBus; import com.google.common.eventbus.AsyncEventBus;
import com.google.common.eventbus.EventBus; import com.google.common.eventbus.EventBus;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javax.annotation.Nonnull;
import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.openide.util.NbBundle; import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent;
import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent;
import org.sleuthkit.autopsy.casemodule.services.TagsManager; import org.sleuthkit.autopsy.casemodule.services.TagsManager;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.ContentTag;
import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.TagName;
@ -44,39 +42,38 @@ import org.sleuthkit.datamodel.TskCoreException;
/** /**
* Manages Tags, Tagging, and the relationship between Categories and Tags in * Manages Tags, Tagging, and the relationship between Categories and Tags in
* the autopsy Db. Delegates some work to the backing {@link TagsManager}. * the autopsy Db. Delegates some work to the backing autopsy TagsManager.
*/ */
@NbBundle.Messages({"DrawableTagsManager.followUp=Follow Up", @NbBundle.Messages({"DrawableTagsManager.followUp=Follow Up",
"DrawableTagsManager.bookMark=Bookmark"}) "DrawableTagsManager.bookMark=Bookmark"})
public class DrawableTagsManager { public final class DrawableTagsManager {
private static final Logger LOGGER = Logger.getLogger(DrawableTagsManager.class.getName()); private static final Logger logger = Logger.getLogger(DrawableTagsManager.class.getName());
private static Image FOLLOW_UP_IMAGE; private static final Image FOLLOW_UP_IMAGE = new Image("/org/sleuthkit/autopsy/imagegallery/images/flag_red.png");
private static Image BOOKMARK_IMAGE; private static final Image BOOKMARK_IMAGE = new Image("/org/sleuthkit/autopsy/images/star-bookmark-icon-16.png");
final private Object autopsyTagsManagerLock = new Object(); private final TagsManager autopsyTagsManager;
private TagsManager autopsyTagsManager; /** The tag name corresponding to the "built-in" tag "Follow Up" */
private final TagName followUpTagName;
private final TagName bookmarkTagName;
/** /**
* Used to distribute {@link TagsChangeEvent}s * Used to distribute TagsChangeEvents
*/ */
private final EventBus tagsEventBus = new AsyncEventBus( private final EventBus tagsEventBus
= new AsyncEventBus(
Executors.newSingleThreadExecutor( Executors.newSingleThreadExecutor(
new BasicThreadFactory.Builder().namingPattern("Tags Event Bus").uncaughtExceptionHandler((Thread t, Throwable e) -> { //NON-NLS new BasicThreadFactory.Builder()
LOGGER.log(Level.SEVERE, "uncaught exception in event bus handler", e); //NON-NLS .namingPattern("Tags Event Bus")//NON-NLS
}).build() .uncaughtExceptionHandler((Thread thread, Throwable throwable)
)); -> logger.log(Level.SEVERE, "Uncaught exception in DrawableTagsManager event bus handler.", throwable)) //NON-NLS
.build()));
/**
* The tag name corresponding to the "built-in" tag "Follow Up"
*/
private TagName followUpTagName;
private TagName bookmarkTagName;
public DrawableTagsManager(TagsManager autopsyTagsManager) {
this.autopsyTagsManager = autopsyTagsManager;
public DrawableTagsManager(ImageGalleryController controller) throws TskCoreException {
this.autopsyTagsManager = controller.getAutopsyCase().getServices().getTagsManager();
followUpTagName = getTagName(Bundle.DrawableTagsManager_followUp());
bookmarkTagName = getTagName(Bundle.DrawableTagsManager_bookMark());
} }
/** /**
@ -106,74 +103,51 @@ public class DrawableTagsManager {
} }
/** /**
* assign a new TagsManager to back this one, ie when the current case * Get the follow up TagName.
* changes
* *
* @param autopsyTagsManager * @return The follow up TagName.
*/ */
public void setAutopsyTagsManager(TagsManager autopsyTagsManager) { public TagName getFollowUpTagName() {
synchronized (autopsyTagsManagerLock) {
this.autopsyTagsManager = autopsyTagsManager;
clearFollowUpTagName();
}
}
/**
* Use when closing a case to make sure everything is re-initialized in the
* next case.
*/
public void clearFollowUpTagName() {
synchronized (autopsyTagsManagerLock) {
followUpTagName = null;
}
}
/**
* get the (cached) follow up TagName
*
* @return
*
* @throws TskCoreException
*/
public TagName getFollowUpTagName() throws TskCoreException {
synchronized (autopsyTagsManagerLock) {
if (Objects.isNull(followUpTagName)) {
followUpTagName = getTagName(NbBundle.getMessage(DrawableTagsManager.class, "DrawableTagsManager.followUp"));
}
return followUpTagName; return followUpTagName;
} }
}
private Object getBookmarkTagName() throws TskCoreException {
synchronized (autopsyTagsManagerLock) {
if (Objects.isNull(bookmarkTagName)) {
bookmarkTagName = getTagName(NbBundle.getMessage(DrawableTagsManager.class, "DrawableTagsManager.bookMark"));
}
return bookmarkTagName;
}
}
/** /**
* get all the TagNames that are not categories * Get the bookmark TagName.
* *
* @return all the TagNames that are not categories, in alphabetical order * @return The bookmark TagName.
* by displayName, or, an empty set if there was an exception
* looking them up from the db.
*/ */
@Nonnull private TagName getBookmarkTagName() throws TskCoreException {
public List<TagName> getNonCategoryTagNames() { return bookmarkTagName;
synchronized (autopsyTagsManagerLock) { }
try {
/**
* Get all the TagNames that are not categories
*
* @return All the TagNames that are not categories, in alphabetical order
* by displayName.
*
* @throws org.sleuthkit.datamodel.TskCoreException
*/
public List<TagName> getNonCategoryTagNames() throws TskCoreException {
return autopsyTagsManager.getAllTagNames().stream() return autopsyTagsManager.getAllTagNames().stream()
.filter(CategoryManager::isNotCategoryTagName) .filter(CategoryManager::isNotCategoryTagName)
.distinct().sorted() .distinct().sorted()
.collect(Collectors.toList()); .collect(Collectors.toList());
} catch (TskCoreException | IllegalStateException ex) {
LOGGER.log(Level.WARNING, "couldn't access case", ex); //NON-NLS
}
return Collections.emptyList();
} }
/**
* Get all the TagNames that are categories
*
* @return All the TagNames that are categories, in alphabetical order by
* displayName.
*
* @throws org.sleuthkit.datamodel.TskCoreException
*/
public List<TagName> getCategoryTagNames() throws TskCoreException {
return autopsyTagsManager.getAllTagNames().stream()
.filter(CategoryManager::isCategoryTagName)
.distinct().sorted()
.collect(Collectors.toList());
} }
/** /**
@ -187,10 +161,8 @@ public class DrawableTagsManager {
* @throws TskCoreException if there was an error reading from the db * @throws TskCoreException if there was an error reading from the db
*/ */
public List<ContentTag> getContentTags(Content content) throws TskCoreException { public List<ContentTag> getContentTags(Content content) throws TskCoreException {
synchronized (autopsyTagsManagerLock) {
return autopsyTagsManager.getContentTagsByContent(content); return autopsyTagsManager.getContentTagsByContent(content);
} }
}
/** /**
* Gets content tags by DrawableFile. * Gets content tags by DrawableFile.
@ -207,8 +179,7 @@ public class DrawableTagsManager {
} }
public TagName getTagName(String displayName) throws TskCoreException { public TagName getTagName(String displayName) throws TskCoreException {
synchronized (autopsyTagsManagerLock) {
try {
TagName returnTagName = autopsyTagsManager.getDisplayNamesToTagNamesMap().get(displayName); TagName returnTagName = autopsyTagsManager.getDisplayNamesToTagNamesMap().get(displayName);
if (returnTagName != null) { if (returnTagName != null) {
return returnTagName; return returnTagName;
@ -222,76 +193,42 @@ public class DrawableTagsManager {
} }
throw new TskCoreException("Tag name exists but an error occured in retrieving it", ex); throw new TskCoreException("Tag name exists but an error occured in retrieving it", ex);
} }
} catch (NullPointerException | IllegalStateException ex) {
LOGGER.log(Level.SEVERE, "Case was closed out from underneath", ex); //NON-NLS
throw new TskCoreException("Case was closed out from underneath", ex);
}
}
} }
public TagName getTagName(DhsImageCategory cat) { public TagName getTagName(DhsImageCategory cat) throws TskCoreException {
try {
return getTagName(cat.getDisplayName()); return getTagName(cat.getDisplayName());
} catch (TskCoreException ex) {
return null;
}
} }
public ContentTag addContentTag(DrawableFile file, TagName tagName, String comment) throws TskCoreException { public ContentTag addContentTag(DrawableFile file, TagName tagName, String comment) throws TskCoreException {
synchronized (autopsyTagsManagerLock) {
return autopsyTagsManager.addContentTag(file.getAbstractFile(), tagName, comment); return autopsyTagsManager.addContentTag(file.getAbstractFile(), tagName, comment);
} }
}
public List<ContentTag> getContentTagsByTagName(TagName t) throws TskCoreException { public List<ContentTag> getContentTagsByTagName(TagName tagName) throws TskCoreException {
synchronized (autopsyTagsManagerLock) { return autopsyTagsManager.getContentTagsByTagName(tagName);
return autopsyTagsManager.getContentTagsByTagName(t);
}
} }
public List<TagName> getAllTagNames() throws TskCoreException { public List<TagName> getAllTagNames() throws TskCoreException {
synchronized (autopsyTagsManagerLock) {
return autopsyTagsManager.getAllTagNames(); return autopsyTagsManager.getAllTagNames();
} }
}
public List<TagName> getTagNamesInUse() throws TskCoreException { public List<TagName> getTagNamesInUse() throws TskCoreException {
synchronized (autopsyTagsManagerLock) {
return autopsyTagsManager.getTagNamesInUse(); return autopsyTagsManager.getTagNamesInUse();
} }
}
public void deleteContentTag(ContentTag ct) throws TskCoreException { public void deleteContentTag(ContentTag contentTag) throws TskCoreException {
synchronized (autopsyTagsManagerLock) { autopsyTagsManager.deleteContentTag(contentTag);
autopsyTagsManager.deleteContentTag(ct);
}
} }
public Node getGraphic(TagName tagname) { public Node getGraphic(TagName tagname) {
try { try {
if (tagname.equals(getFollowUpTagName())) { if (tagname.equals(getFollowUpTagName())) {
return new ImageView(getFollowUpImage()); return new ImageView(FOLLOW_UP_IMAGE);
} else if (tagname.equals(getBookmarkTagName())) { } else if (tagname.equals(getBookmarkTagName())) {
return new ImageView(getBookmarkImage()); return new ImageView(BOOKMARK_IMAGE);
} }
} catch (TskCoreException ex) { } catch (TskCoreException ex) {
LOGGER.log(Level.SEVERE, "Failed to get \"Follow Up\" or \"Bookmark\"tag name from db.", ex); logger.log(Level.SEVERE, "Failed to get \"Follow Up\" or \"Bookmark\"tag name from db.", ex);
} }
return DrawableAttribute.TAGS.getGraphicForValue(tagname); return DrawableAttribute.TAGS.getGraphicForValue(tagname);
} }
synchronized private static Image getFollowUpImage() {
if (FOLLOW_UP_IMAGE == null) {
FOLLOW_UP_IMAGE = new Image("/org/sleuthkit/autopsy/imagegallery/images/flag_red.png");
}
return FOLLOW_UP_IMAGE;
}
synchronized private static Image getBookmarkImage() {
if (BOOKMARK_IMAGE == null) {
BOOKMARK_IMAGE = new Image("/org/sleuthkit/autopsy/images/star-bookmark-icon-16.png");
}
return BOOKMARK_IMAGE;
}
} }

View File

@ -1,8 +1,26 @@
package org.sleuthkit.autopsy.imagegallery.datamodel; /*
* Autopsy Forensic Browser
*
* Copyright 2013-18 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.datamodel;
import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader; import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache; import com.google.common.cache.LoadingCache;
import java.sql.SQLException;
import java.util.Collections; import java.util.Collections;
import java.util.Set; import java.util.Set;
import java.util.logging.Level; import java.util.logging.Level;
@ -15,26 +33,18 @@ import org.sleuthkit.datamodel.TskCoreException;
*/ */
public class HashSetManager { public class HashSetManager {
/** /** The db that initial values are loaded from. */
* The db that initial values are loaded from. private final DrawableDB drawableDB;
*/
private DrawableDB db = null; public HashSetManager(DrawableDB drawableDB) {
this.drawableDB = drawableDB;
}
/** /**
* the internal cache from fileID to a set of hashset names. * the internal cache from fileID to a set of hashset names.
*/ */
private final LoadingCache<Long, Set<String>> hashSetCache = CacheBuilder.newBuilder().build(CacheLoader.from(this::getHashSetsForFileHelper)); private final LoadingCache<Long, Set<String>> hashSetCache = CacheBuilder.newBuilder().build(CacheLoader.from(this::getHashSetsForFileHelper));
/**
* assign the given db to back this hashset manager.
*
* @param db
*/
public void setDb(DrawableDB db) {
this.db = db;
hashSetCache.invalidateAll();
}
/** /**
* helper method to load hashset hits for the given fileID from the db * helper method to load hashset hits for the given fileID from the db
* *
@ -44,9 +54,14 @@ public class HashSetManager {
*/ */
private Set<String> getHashSetsForFileHelper(long fileID) { private Set<String> getHashSetsForFileHelper(long fileID) {
try { try {
return db.getHashSetsForFile(fileID); if (drawableDB.isClosed()) {
} catch (TskCoreException ex) { Logger.getLogger(HashSetManager.class.getName()).log(Level.WARNING, "Failed to get Hash Sets for file. The Db connection was already closed."); //NON-NLS
Logger.getLogger(HashSetManager.class.getName()).log(Level.SEVERE, "Failed to get Hash Sets for file", ex); //NON-NLS return Collections.emptySet();
} else {
return drawableDB.getHashSetsForFile(fileID);
}
} catch (TskCoreException | SQLException ex) {
Logger.getLogger(HashSetManager.class.getName()).log(Level.SEVERE, "Failed to get Hash Sets for file."); //NON-NLS
return Collections.emptySet(); return Collections.emptySet();
} }
} }

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2013-16 Basis Technology Corp. * Copyright 2013-18 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");
@ -26,16 +26,19 @@ import java.util.logging.Level;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding; import javafx.beans.binding.DoubleBinding;
import javafx.beans.binding.IntegerBinding; import javafx.beans.binding.IntegerBinding;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.ReadOnlyLongProperty; import javafx.beans.property.ReadOnlyLongProperty;
import javafx.beans.property.ReadOnlyLongWrapper; import javafx.beans.property.ReadOnlyLongWrapper;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.ImageGalleryModule;
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.DrawableAttribute;
import org.sleuthkit.datamodel.TskCoreException;
/** /**
* Represents a set of image/video files in a group. The UI listens to changes * Represents a set of image/video files in a group. The UI listens to changes
@ -76,7 +79,7 @@ public class DrawableGroup implements Comparable<DrawableGroup> {
} }
@SuppressWarnings("ReturnOfCollectionOrArrayField") @SuppressWarnings("ReturnOfCollectionOrArrayField")
public synchronized ObservableList<Long> getFileIDs() { public ObservableList<Long> getFileIDs() {
return unmodifiableFileIDS; return unmodifiableFileIDS;
} }
@ -121,11 +124,11 @@ public class DrawableGroup implements Comparable<DrawableGroup> {
if (hashSetHitsCount.get() < 0) { if (hashSetHitsCount.get() < 0) {
try { try {
hashSetHitsCount.set(fileIDs.stream() hashSetHitsCount.set(fileIDs.stream()
.map(fileID -> ImageGalleryController.getDefault().getHashSetManager().isInAnyHashSet(fileID)) .map(ImageGalleryModule.getController().getHashSetManager()::isInAnyHashSet)
.filter(Boolean::booleanValue) .filter(Boolean::booleanValue)
.count()); .count());
} catch (IllegalStateException | NullPointerException ex) { } catch (NoCurrentCaseException ex) {
LOGGER.log(Level.WARNING, "could not access case during getFilesWithHashSetHitsCount()"); //NON-NLS LOGGER.log(Level.WARNING, "Could not access case during getFilesWithHashSetHitsCount()"); //NON-NLS
} }
} }
return hashSetHitsCount.get(); return hashSetHitsCount.get();
@ -139,10 +142,10 @@ public class DrawableGroup implements Comparable<DrawableGroup> {
public final synchronized long getUncategorizedCount() { public final synchronized long getUncategorizedCount() {
if (uncatCount.get() < 0) { if (uncatCount.get() < 0) {
try { try {
uncatCount.set(ImageGalleryController.getDefault().getDatabase().getUncategorizedCount(fileIDs)); uncatCount.set(ImageGalleryModule.getController().getDatabase().getUncategorizedCount(fileIDs));
} catch (IllegalStateException | NullPointerException ex) { } catch (TskCoreException | NoCurrentCaseException ex) {
LOGGER.log(Level.WARNING, "could not access case during getFilesWithHashSetHitsCount()"); //NON-NLS LOGGER.log(Level.WARNING, "Could not access case during getFilesWithHashSetHitsCount()"); //NON-NLS
} }
} }
@ -163,8 +166,8 @@ public class DrawableGroup implements Comparable<DrawableGroup> {
return seen.get(); return seen.get();
} }
public ReadOnlyBooleanWrapper seenProperty() { public ReadOnlyBooleanProperty seenProperty() {
return seen; return seen.getReadOnlyProperty();
} }
@Subscribe @Subscribe

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2013-16 Basis Technology Corp. * Copyright 2013-18 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");
@ -18,27 +18,31 @@
*/ */
package org.sleuthkit.autopsy.imagegallery.datamodel.grouping; package org.sleuthkit.autopsy.imagegallery.datamodel.grouping;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import javafx.scene.Node; import javafx.scene.Node;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute;
import org.sleuthkit.datamodel.DataSource;
import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.TagName;
/** /**
* key identifying information of a {@link Grouping}. Used to look up groups in * Key identifying information of a DrawableGroup. Used to look up groups in
* {@link Map}s and from the db. * Maps and from the db.
*
* @param <T> The type of the values of the attribute this key uses.
*/ */
@Immutable @Immutable
public class GroupKey<T extends Comparable<T>> implements Comparable<GroupKey<T>> { public class GroupKey<T extends Comparable<T>> implements Comparable<GroupKey<T>> {
private final T val; private final T val;
private final DrawableAttribute<T> attr; private final DrawableAttribute<T> attr;
private final DataSource dataSource;
public GroupKey(DrawableAttribute<T> attr, T val) { public GroupKey(DrawableAttribute<T> attr, T val, DataSource dataSource) {
this.attr = attr; this.attr = attr;
this.val = val; this.val = val;
this.dataSource = dataSource;
} }
public T getValue() { public T getValue() {
@ -49,6 +53,10 @@ public class GroupKey<T extends Comparable<T>> implements Comparable<GroupKey<T>
return attr; return attr;
} }
public Optional< DataSource> getDataSource() {
return Optional.ofNullable(dataSource);
}
public String getValueDisplayName() { public String getValueDisplayName() {
return Objects.equals(attr, DrawableAttribute.TAGS) return Objects.equals(attr, DrawableAttribute.TAGS)
? ((TagName) getValue()).getDisplayName() ? ((TagName) getValue()).getDisplayName()
@ -63,13 +71,19 @@ public class GroupKey<T extends Comparable<T>> implements Comparable<GroupKey<T>
@Override @Override
public int hashCode() { public int hashCode() {
int hash = 5; int hash = 5;
hash = 29 * hash + Objects.hashCode(this.val);
hash = 29 * hash + Objects.hashCode(this.attr); hash = 79 * hash + Objects.hashCode(this.val);
hash = 79 * hash + Objects.hashCode(this.attr);
hash = 79 * hash + Objects.hashCode(this.dataSource);
return hash; return hash;
} }
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) { if (obj == null) {
return false; return false;
} }
@ -77,11 +91,14 @@ public class GroupKey<T extends Comparable<T>> implements Comparable<GroupKey<T>
return false; return false;
} }
final GroupKey<?> other = (GroupKey<?>) obj; final GroupKey<?> other = (GroupKey<?>) obj;
if (this.attr != other.attr) { if (!Objects.equals(this.val, other.val)) {
return false; return false;
} }
return Objects.equals(this.val, other.val); if (!Objects.equals(this.attr, other.attr)) {
return false;
}
return Objects.equals(this.dataSource, other.dataSource);
} }
@Override @Override
@ -92,4 +109,8 @@ public class GroupKey<T extends Comparable<T>> implements Comparable<GroupKey<T>
public Node getGraphic() { public Node getGraphic() {
return attr.getGraphicForValue(val); return attr.getGraphicForValue(val);
} }
public long getDataSourceObjId() {
return getDataSource().map(DataSource::getId).orElse(0L);
}
} }

View File

@ -28,7 +28,8 @@ import org.openide.util.NbBundle;
/** /**
* Pseudo enum of possible properties to sort groups by. * Pseudo enum of possible properties to sort groups by.
*/ */
@NbBundle.Messages({"GroupSortBy.groupSize=Group Size", @NbBundle.Messages({
"GroupSortBy.groupSize=Group Size",
"GroupSortBy.groupName=Group Name", "GroupSortBy.groupName=Group Name",
"GroupSortBy.none=None", "GroupSortBy.none=None",
"GroupSortBy.priority=Priority"}) "GroupSortBy.priority=Priority"})
@ -37,40 +38,35 @@ public class GroupSortBy implements Comparator<DrawableGroup> {
/** /**
* sort the groups by the number of files in each * sort the groups by the number of files in each
*/ */
public final static GroupSortBy FILE_COUNT = public final static GroupSortBy FILE_COUNT
new GroupSortBy(Bundle.GroupSortBy_groupSize(), "folder-open-image.png", = new GroupSortBy(Bundle.GroupSortBy_groupSize(), "folder-open-image.png",
Comparator.comparing(DrawableGroup::getSize)); Comparator.comparing(DrawableGroup::getSize));
/** /**
* sort the groups by the natural order of the grouping value ( eg group * sort the groups by the natural order of the grouping value ( eg group
* them by path alphabetically ) * them by path alphabetically )
*/ */
public final static GroupSortBy GROUP_BY_VALUE = public final static GroupSortBy GROUP_BY_VALUE
new GroupSortBy(Bundle.GroupSortBy_groupName(), "folder-rename.png", = new GroupSortBy(Bundle.GroupSortBy_groupName(), "folder-rename.png",
Comparator.comparing(DrawableGroup::getGroupByValueDislpayName)); Comparator.comparing(DrawableGroup::getGroupByValueDislpayName));
/** /**
* don't sort the groups just use what ever order they come in (ingest * don't sort the groups just use what ever order they come in (ingest
* order) * order)
*/ */
public final static GroupSortBy NONE = public final static GroupSortBy NONE
new GroupSortBy(Bundle.GroupSortBy_none(), "prohibition.png", = new GroupSortBy(Bundle.GroupSortBy_none(), "prohibition.png",
new AllEqualComparator<>()); new AllEqualComparator<>());
/** /**
* sort the groups by some priority metric to be determined and implemented * sort the groups by some priority metric to be determined and implemented
*/ */
public final static GroupSortBy PRIORITY = public final static GroupSortBy PRIORITY
new GroupSortBy(Bundle.GroupSortBy_priority(), "hashset_hits.png", = new GroupSortBy(Bundle.GroupSortBy_priority(), "hashset_hits.png",
Comparator.comparing(DrawableGroup::getHashHitDensity) Comparator.comparing(DrawableGroup::getHashHitDensity)
.thenComparing(Comparator.comparing(DrawableGroup::getUncategorizedCount)) .thenComparing(Comparator.comparing(DrawableGroup::getUncategorizedCount))
.reversed()); .reversed());
@Override
public int compare(DrawableGroup o1, DrawableGroup o2) {
return delegate.compare(o1, o2);
}
private final static ObservableList<GroupSortBy> values = FXCollections.unmodifiableObservableList(FXCollections.observableArrayList(PRIORITY, NONE, GROUP_BY_VALUE, FILE_COUNT)); private final static ObservableList<GroupSortBy> values = FXCollections.unmodifiableObservableList(FXCollections.observableArrayList(PRIORITY, NONE, GROUP_BY_VALUE, FILE_COUNT));
/** /**
@ -109,6 +105,11 @@ public class GroupSortBy implements Comparator<DrawableGroup> {
return icon; return icon;
} }
@Override
public int compare(DrawableGroup o1, DrawableGroup o2) {
return delegate.compare(o1, o2);
}
static class AllEqualComparator<A> implements Comparator<A> { static class AllEqualComparator<A> implements Comparator<A> {
@Override @Override

View File

@ -22,9 +22,9 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
/** /**
* * Encapsulate information about the state of the group section of the UI.
*/ */
public class GroupViewState { public final class GroupViewState {
private final DrawableGroup group; private final DrawableGroup group;
@ -32,8 +32,8 @@ public class GroupViewState {
private final Optional<Long> slideShowfileID; private final Optional<Long> slideShowfileID;
public DrawableGroup getGroup() { public Optional<DrawableGroup> getGroup() {
return group; return Optional.ofNullable(group);
} }
public GroupViewMode getMode() { public GroupViewMode getMode() {
@ -44,18 +44,18 @@ public class GroupViewState {
return slideShowfileID; return slideShowfileID;
} }
private GroupViewState(DrawableGroup g, GroupViewMode mode, Long slideShowfileID) { private GroupViewState(DrawableGroup group, GroupViewMode mode, Long slideShowfileID) {
this.group = g; this.group = group;
this.mode = mode; this.mode = mode;
this.slideShowfileID = Optional.ofNullable(slideShowfileID); this.slideShowfileID = Optional.ofNullable(slideShowfileID);
} }
public static GroupViewState tile(DrawableGroup g) { public static GroupViewState tile(DrawableGroup group) {
return new GroupViewState(g, GroupViewMode.TILE, null); return new GroupViewState(group, GroupViewMode.TILE, null);
} }
public static GroupViewState slideShow(DrawableGroup g, Long fileID) { public static GroupViewState slideShow(DrawableGroup group, Long fileID) {
return new GroupViewState(g, GroupViewMode.SLIDE_SHOW, fileID); return new GroupViewState(group, GroupViewMode.SLIDE_SHOW, fileID);
} }
@Override @Override
@ -82,10 +82,7 @@ public class GroupViewState {
if (this.mode != other.mode) { if (this.mode != other.mode) {
return false; return false;
} }
if (!Objects.equals(this.slideShowfileID, other.slideShowfileID)) { return Objects.equals(this.slideShowfileID, other.slideShowfileID);
return false;
}
return true;
} }
} }

View File

@ -18,20 +18,43 @@
*/ */
package org.sleuthkit.autopsy.imagegallery.gui; package org.sleuthkit.autopsy.imagegallery.gui;
import java.io.IOException;
import java.net.URL;
import java.util.logging.Level;
import javafx.scene.control.ButtonBase; import javafx.scene.control.ButtonBase;
import javafx.scene.control.Dialog;
import javafx.scene.control.MenuItem; import javafx.scene.control.MenuItem;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import org.controlsfx.control.action.Action; import org.controlsfx.control.action.Action;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
/** /**
* Static utility methods for working with GUI components * Static utility methods for working with GUI components
*/ */
public class GuiUtils { public final class GuiUtils {
private final static Logger logger = Logger.getLogger(GuiUtils.class.getName());
/** Image to use as title bar icon in dialogs */
private static final Image AUTOPSY_ICON;
static {
Image tempImg = null;
try {
tempImg = new Image(new URL("nbresloc:/org/netbeans/core/startup/frame.gif").openStream()); //NON-NLS
} catch (IOException ex) {
logger.log(Level.WARNING, "Failed to load branded icon for progress dialog.", ex); //NON-NLS
}
AUTOPSY_ICON = tempImg;
}
private GuiUtils() { private GuiUtils() {
} }
/** /**
* create a MenuItem that performs the given action and also set the Action * Create a MenuItem that performs the given action and also set the Action
* as the action for the given Button. Usefull to have a SplitMenuButton * as the action for the given Button. Usefull to have a SplitMenuButton
* remember the last chosen menu item as its action. * remember the last chosen menu item as its action.
* *
@ -51,4 +74,14 @@ public class GuiUtils {
}); });
return menuItem; return menuItem;
} }
/**
* Set the title bar icon for the given Dialog to be the Autopsy logo icon.
*
* @param dialog The dialog to set the title bar icon for.
*/
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
public static void setDialogIcons(Dialog<?> dialog) {
((Stage) dialog.getDialogPane().getScene().getWindow()).getIcons().setAll(AUTOPSY_ICON);
}
} }

View File

@ -79,7 +79,7 @@ public class StatusBar extends AnchorPane {
}); });
Platform.runLater(() -> staleLabel.setTooltip(new Tooltip(Bundle.StatuBar_toolTip()))); Platform.runLater(() -> staleLabel.setTooltip(new Tooltip(Bundle.StatuBar_toolTip())));
staleLabel.visibleProperty().bind(controller.stale()); staleLabel.visibleProperty().bind(controller.staleProperty());
} }
public StatusBar(ImageGalleryController controller) { public StatusBar(ImageGalleryController controller) {

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2013-15 Basis Technology Corp. * Copyright 2013-18 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");
@ -34,9 +34,10 @@ import javafx.scene.layout.VBox;
import javafx.util.Pair; import javafx.util.Pair;
import org.openide.util.NbBundle; import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.imagegallery.FXMLConstructor; import org.sleuthkit.autopsy.imagegallery.FXMLConstructor;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory; import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager.CategoryChangeEvent;
/** /**
* Displays summary statistics (counts) for each group * Displays summary statistics (counts) for each group
@ -51,10 +52,12 @@ public class SummaryTablePane extends AnchorPane {
@FXML @FXML
private TableView<Pair<DhsImageCategory, Long>> tableView; private TableView<Pair<DhsImageCategory, Long>> tableView;
private final ImageGalleryController controller; private final ImageGalleryController controller;
@FXML @FXML
@NbBundle.Messages({"SummaryTablePane.catColumn=Category", @NbBundle.Messages({
"SummaryTablePane.catColumn=Category",
"SummaryTablePane.countColumn=# Files"}) "SummaryTablePane.countColumn=# Files"})
void initialize() { void initialize() {
assert catColumn != null : "fx:id=\"catColumn\" was not injected: check your FXML file 'SummaryTablePane.fxml'."; assert catColumn != null : "fx:id=\"catColumn\" was not injected: check your FXML file 'SummaryTablePane.fxml'.";
@ -67,11 +70,11 @@ public class SummaryTablePane extends AnchorPane {
tableView.prefHeightProperty().set(7 * 25); tableView.prefHeightProperty().set(7 * 25);
//set up columns //set up columns
catColumn.setCellValueFactory((TableColumn.CellDataFeatures<Pair<DhsImageCategory, Long>, String> p) -> new SimpleObjectProperty<>(p.getValue().getKey().getDisplayName())); catColumn.setCellValueFactory(params -> new SimpleObjectProperty<>(params.getValue().getKey().getDisplayName()));
catColumn.setPrefWidth(USE_COMPUTED_SIZE); catColumn.setPrefWidth(USE_COMPUTED_SIZE);
catColumn.setText(Bundle.SummaryTablePane_catColumn()); catColumn.setText(Bundle.SummaryTablePane_catColumn());
countColumn.setCellValueFactory((TableColumn.CellDataFeatures<Pair<DhsImageCategory, Long>, Long> p) -> new SimpleObjectProperty<>(p.getValue().getValue())); countColumn.setCellValueFactory(params -> new SimpleObjectProperty<>(params.getValue().getValue()));
countColumn.setPrefWidth(USE_COMPUTED_SIZE); countColumn.setPrefWidth(USE_COMPUTED_SIZE);
countColumn.setText(Bundle.SummaryTablePane_countColumn()); countColumn.setText(Bundle.SummaryTablePane_countColumn());
@ -85,14 +88,15 @@ public class SummaryTablePane extends AnchorPane {
public SummaryTablePane(ImageGalleryController controller) { public SummaryTablePane(ImageGalleryController controller) {
this.controller = controller; this.controller = controller;
FXMLConstructor.construct(this, "SummaryTablePane.fxml"); //NON-NLS FXMLConstructor.construct(this, "SummaryTablePane.fxml"); //NON-NLS
} }
/** /**
* listen to Category updates and rebuild the table * listen to Category updates and rebuild the table
*
* @param evt The change event.
*/ */
@Subscribe @Subscribe
public void handleCategoryChanged(org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager.CategoryChangeEvent evt) { public void handleCategoryChanged(CategoryChangeEvent evt) {
final ObservableList<Pair<DhsImageCategory, Long>> data = FXCollections.observableArrayList(); final ObservableList<Pair<DhsImageCategory, Long>> data = FXCollections.observableArrayList();
if (Case.isCaseOpen()) { if (Case.isCaseOpen()) {
for (DhsImageCategory cat : DhsImageCategory.values()) { for (DhsImageCategory cat : DhsImageCategory.values()) {

View File

@ -12,17 +12,14 @@
<?import javafx.scene.image.ImageView?> <?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.HBox?> <?import javafx.scene.layout.HBox?>
<fx:root minWidth="-1.0" orientation="HORIZONTAL" prefWidth="-1.0" type="javafx.scene.control.ToolBar" xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1"> <fx:root minWidth="-1.0" orientation="HORIZONTAL" prefWidth="-1.0" type="javafx.scene.control.ToolBar" xmlns="http://javafx.com/javafx/8.0.141" xmlns:fx="http://javafx.com/fxml/1">
<items> <items>
<HBox alignment="CENTER" spacing="5.0"> <HBox alignment="CENTER" spacing="5.0">
<children> <children>
<Label fx:id="groupByLabel" text="Group By:"> <Label fx:id="groupByLabel" text="Group By:">
<labelFor>
<ComboBox fx:id="groupByBox" editable="false" />
</labelFor>
</Label>
<fx:reference source="groupByBox" />
</Label>
<ComboBox fx:id="groupByBox" prefWidth="100.0" />
</children> </children>
</HBox> </HBox>
<ImageView fx:id="sortHelpImageView" fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true"> <ImageView fx:id="sortHelpImageView" fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
@ -30,11 +27,26 @@
<Image url="@../images/question-frame.png" /> <Image url="@../images/question-frame.png" />
</image> </image>
</ImageView> </ImageView>
<HBox alignment="CENTER" spacing="5.0">
<children>
<Label mnemonicParsing="false" text=" Data Source: ">
<graphic>
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../images/datasource.png" />
</image>
</ImageView>
</graphic>
</Label>
<ComboBox fx:id="dataSourceComboBox" editable="false" maxWidth="200.0" />
</children>
</HBox>
<Separator orientation="VERTICAL" prefHeight="-1.0" prefWidth="10.0" /> <Separator orientation="VERTICAL" prefHeight="-1.0" prefWidth="20.0" />
<HBox alignment="CENTER" spacing="5.0"> <HBox alignment="CENTER" spacing="5.0">
<children> <children>
<Label fx:id="tagImageViewLabel" text="Tag Group's Files:"> <Label fx:id="tagImageViewLabel" text="Tag Group's Files:">
@ -76,7 +88,7 @@
<Insets left="5.0" /> <Insets left="5.0" />
</padding> </padding>
</HBox> </HBox>
<Separator orientation="VERTICAL" prefHeight="-1.0" prefWidth="10.0" /> <Separator orientation="VERTICAL" prefHeight="-1.0" prefWidth="20.0" />
<HBox alignment="CENTER" spacing="5.0"> <HBox alignment="CENTER" spacing="5.0">
<children> <children>
<Label fx:id="thumbnailSizeLabel" text="Thumbnail Size (px):"> <Label fx:id="thumbnailSizeLabel" text="Thumbnail Size (px):">

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2013-16 Basis Technology Corp. * Copyright 2013-18 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");
@ -18,22 +18,33 @@
*/ */
package org.sleuthkit.autopsy.imagegallery.gui; package org.sleuthkit.autopsy.imagegallery.gui;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.logging.Level; import java.util.logging.Level;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.InvalidationListener; import javafx.beans.InvalidationListener;
import javafx.beans.Observable; import javafx.beans.Observable;
import javafx.beans.property.DoubleProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.scene.Cursor; import javafx.scene.Cursor;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ComboBox; import javafx.scene.control.ComboBox;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.MenuItem; import javafx.scene.control.MenuItem;
import javafx.scene.control.SingleSelectionModel;
import javafx.scene.control.Slider; import javafx.scene.control.Slider;
import javafx.scene.control.SplitMenuButton; import javafx.scene.control.SplitMenuButton;
import javafx.scene.control.ToolBar; import javafx.scene.control.ToolBar;
@ -42,63 +53,70 @@ import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane; import javafx.scene.layout.Pane;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import javafx.stage.Modality;
import javafx.util.StringConverter;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import org.controlsfx.control.PopOver; import org.controlsfx.control.PopOver;
import org.openide.util.NbBundle; import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.Case;
import static org.sleuthkit.autopsy.casemodule.Case.Events.DATA_SOURCE_ADDED;
import static org.sleuthkit.autopsy.casemodule.Case.Events.DATA_SOURCE_DELETED;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.imagegallery.FXMLConstructor; import org.sleuthkit.autopsy.imagegallery.FXMLConstructor;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.actions.CategorizeGroupAction; import org.sleuthkit.autopsy.imagegallery.actions.CategorizeGroupAction;
import org.sleuthkit.autopsy.imagegallery.actions.TagGroupAction; import org.sleuthkit.autopsy.imagegallery.actions.TagGroupAction;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute; 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.GroupSortBy; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupSortBy;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState;
import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.autopsy.imagegallery.utils.TaskUtils;
import org.sleuthkit.datamodel.DataSource;
/** /**
* Controller for the ToolBar * Controller for the ToolBar
*/ */
public class Toolbar extends ToolBar { public class Toolbar extends ToolBar {
private static final Logger LOGGER = Logger.getLogger(Toolbar.class.getName()); private static final Logger logger = Logger.getLogger(Toolbar.class.getName());
private static final int SIZE_SLIDER_DEFAULT = 100; private static final int SIZE_SLIDER_DEFAULT = 100;
@FXML
private ComboBox<Optional<DataSource>> dataSourceComboBox;
@FXML @FXML
private ImageView sortHelpImageView; private ImageView sortHelpImageView;
@FXML @FXML
private ComboBox<DrawableAttribute<?>> groupByBox; private ComboBox<DrawableAttribute<?>> groupByBox;
@FXML @FXML
private Slider sizeSlider; private Slider sizeSlider;
@FXML @FXML
private SplitMenuButton catGroupMenuButton; private SplitMenuButton catGroupMenuButton;
@FXML @FXML
private SplitMenuButton tagGroupMenuButton; private SplitMenuButton tagGroupMenuButton;
@FXML @FXML
private Label groupByLabel; private Label groupByLabel;
@FXML @FXML
private Label tagImageViewLabel; private Label tagImageViewLabel;
@FXML @FXML
private Label categoryImageViewLabel; private Label categoryImageViewLabel;
@FXML @FXML
private Label thumbnailSizeLabel; private Label thumbnailSizeLabel;
private final ImageGalleryController controller;
private SortChooser<DrawableGroup, GroupSortBy> sortChooser; private SortChooser<DrawableGroup, GroupSortBy> sortChooser;
private final ListeningExecutorService exec = TaskUtils.getExecutorForClass(Toolbar.class);
private final ImageGalleryController controller;
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private final ObservableList<Optional<DataSource>> dataSources = FXCollections.observableArrayList();
private SingleSelectionModel<Optional<DataSource>> dataSourceSelectionModel;
private final InvalidationListener queryInvalidationListener = new InvalidationListener() { private final InvalidationListener queryInvalidationListener = new InvalidationListener() {
public void invalidated(Observable o) { @Override
controller.getGroupManager().regroup( public void invalidated(Observable invalidated) {
controller.getGroupManager().regroup(getSelectedDataSource(),
groupByBox.getSelectionModel().getSelectedItem(), groupByBox.getSelectionModel().getSelectedItem(),
sortChooser.getComparator(), sortChooser.getComparator(),
sortChooser.getSortOrder(), sortChooser.getSortOrder(),
@ -106,12 +124,9 @@ public class Toolbar extends ToolBar {
} }
}; };
public DoubleProperty thumbnailSizeProperty() {
return sizeSlider.valueProperty();
}
@FXML @FXML
@NbBundle.Messages({"Toolbar.groupByLabel=Group By:", @NbBundle.Messages(
{"Toolbar.groupByLabel=Group By:",
"Toolbar.sortByLabel=Sort By:", "Toolbar.sortByLabel=Sort By:",
"Toolbar.ascRadio=Ascending", "Toolbar.ascRadio=Ascending",
"Toolbar.descRadio=Descending", "Toolbar.descRadio=Descending",
@ -119,48 +134,84 @@ public class Toolbar extends ToolBar {
"Toolbar.categoryImageViewLabel=Categorize Group's Files:", "Toolbar.categoryImageViewLabel=Categorize Group's Files:",
"Toolbar.thumbnailSizeLabel=Thumbnail Size (px):", "Toolbar.thumbnailSizeLabel=Thumbnail Size (px):",
"Toolbar.sortHelp=The sort direction (ascending/descending) affects the queue of unseen groups that Image Gallery maintains, but changes to this queue aren't apparent until the \"Next Unseen Group\" button is pressed.", "Toolbar.sortHelp=The sort direction (ascending/descending) affects the queue of unseen groups that Image Gallery maintains, but changes to this queue aren't apparent until the \"Next Unseen Group\" button is pressed.",
"Toolbar.sortHelpTitle=Group Sorting",}) "Toolbar.sortHelpTitle=Group Sorting",
"Toolbar.getDataSources.errMessage=Unable to get datasources for current case.",
"Toolbar.nonPathGroupingWarning.content=Proceed with regrouping?",
"Toolbar.nonPathGroupingWarning.header=Grouping by attributes other than path does not support the data source filter.\nFiles and groups from all data sources will be shown.",
"Toolbar.nonPathGroupingWarning.title=Image Gallery"})
void initialize() { void initialize() {
assert catGroupMenuButton != null : "fx:id=\"catSelectedMenubutton\" was not injected: check your FXML file 'Toolbar.fxml'.";
assert groupByBox != null : "fx:id=\"groupByBox\" was not injected: check your FXML file 'Toolbar.fxml'."; assert groupByBox != null : "fx:id=\"groupByBox\" was not injected: check your FXML file 'Toolbar.fxml'.";
assert dataSourceComboBox != null : "fx:id=\"dataSourceComboBox\" was not injected: check your FXML file 'Toolbar.fxml'.";
assert sortHelpImageView != null : "fx:id=\"sortHelpImageView\" was not injected: check your FXML file 'Toolbar.fxml'.";
assert tagImageViewLabel != null : "fx:id=\"tagImageViewLabel\" was not injected: check your FXML file 'Toolbar.fxml'.";
assert tagGroupMenuButton != null : "fx:id=\"tagGroupMenuButton\" was not injected: check your FXML file 'Toolbar.fxml'.";
assert categoryImageViewLabel != null : "fx:id=\"categoryImageViewLabel\" was not injected: check your FXML file 'Toolbar.fxml'.";
assert catGroupMenuButton != null : "fx:id=\"catGroupMenuButton\" was not injected: check your FXML file 'Toolbar.fxml'.";
assert thumbnailSizeLabel != null : "fx:id=\"thumbnailSizeLabel\" was not injected: check your FXML file 'Toolbar.fxml'.";
assert sizeSlider != null : "fx:id=\"sizeSlider\" was not injected: check your FXML file 'Toolbar.fxml'."; assert sizeSlider != null : "fx:id=\"sizeSlider\" was not injected: check your FXML file 'Toolbar.fxml'.";
assert tagGroupMenuButton != null : "fx:id=\"tagSelectedMenubutton\" was not injected: check your FXML file 'Toolbar.fxml'."; this.dataSourceSelectionModel = dataSourceComboBox.getSelectionModel();
controller.viewState().addListener((observable, oldViewState, newViewState) -> { //set internationalized label text
Platform.runLater(() -> syncGroupControlsEnabledState(newViewState)); groupByLabel.setText(Bundle.Toolbar_groupByLabel());
}); tagImageViewLabel.setText(Bundle.Toolbar_tagImageViewLabel());
syncGroupControlsEnabledState(controller.viewState().get()); categoryImageViewLabel.setText(Bundle.Toolbar_categoryImageViewLabel());
thumbnailSizeLabel.setText(Bundle.Toolbar_thumbnailSizeLabel());
sizeSlider.valueProperty().bindBidirectional(controller.thumbnailSizeProperty());
controller.viewStateProperty().addListener((observable, oldViewState, newViewState)
-> Platform.runLater(() -> syncGroupControlsEnabledState(newViewState))
);
syncGroupControlsEnabledState(controller.viewStateProperty().get());
try { initDataSourceComboBox();
TagGroupAction followUpGroupAction = new TagGroupAction(controller.getTagsManager().getFollowUpTagName(), controller); groupByBox.setItems(FXCollections.observableList(DrawableAttribute.getGroupableAttrs()));
tagGroupMenuButton.setOnAction(followUpGroupAction); groupByBox.getSelectionModel().select(DrawableAttribute.PATH);
tagGroupMenuButton.setText(followUpGroupAction.getText()); groupByBox.disableProperty().bind(controller.regroupDisabledProperty());
tagGroupMenuButton.setGraphic(followUpGroupAction.getGraphic()); groupByBox.setCellFactory(listView -> new AttributeListCell());
} catch (TskCoreException ex) { groupByBox.setButtonCell(new AttributeListCell());
/* groupByBox.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
* The problem appears to be a timing issue where a case is closed if (oldValue == DrawableAttribute.PATH
* before this initialization is completed, which It appears to be && newValue != DrawableAttribute.PATH
* harmless, so we are temporarily changing this log message to a && getSelectedDataSource() != null) {
* WARNING.
* Alert alert = new Alert(Alert.AlertType.CONFIRMATION, Bundle.Toolbar_nonPathGroupingWarning_content());
* TODO (JIRA-3010): SEVERE error logged by image Gallery UI alert.setHeaderText(Bundle.Toolbar_nonPathGroupingWarning_header());
*/ alert.setTitle(Bundle.Toolbar_nonPathGroupingWarning_title());
if (Case.isCaseOpen()) { alert.initModality(Modality.APPLICATION_MODAL);
LOGGER.log(Level.WARNING, "Could not create Follow Up tag menu item", ex); //NON-NLS alert.initOwner(getScene().getWindow());
GuiUtils.setDialogIcons(alert);
if (alert.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) {
queryInvalidationListener.invalidated(observable);
} else {
Platform.runLater(() -> groupByBox.getSelectionModel().select(DrawableAttribute.PATH));
} }
else { } else {
// don't add stack trace to log because it makes looking for real errors harder queryInvalidationListener.invalidated(observable);
LOGGER.log(Level.INFO, "Unable to get tag name. Case is closed."); //NON-NLS
}
}
tagGroupMenuButton.showingProperty().addListener(showing -> {
if (tagGroupMenuButton.isShowing()) {
List<MenuItem> selTagMenues = Lists.transform(controller.getTagsManager().getNonCategoryTagNames(),
tn -> GuiUtils.createAutoAssigningMenuItem(tagGroupMenuButton, new TagGroupAction(tn, controller)));
tagGroupMenuButton.getItems().setAll(selTagMenues);
} }
}); });
sortChooser = new SortChooser<>(GroupSortBy.getValues());
sortChooser.comparatorProperty().addListener((observable, oldComparator, newComparator) -> {
final boolean orderDisabled = newComparator == GroupSortBy.NONE || newComparator == GroupSortBy.PRIORITY;
sortChooser.setSortOrderDisabled(orderDisabled);
final SortChooser.ValueType valueType = newComparator == GroupSortBy.GROUP_BY_VALUE ? SortChooser.ValueType.LEXICOGRAPHIC : SortChooser.ValueType.NUMERIC;
sortChooser.setValueType(valueType);
queryInvalidationListener.invalidated(observable);
});
sortChooser.setComparator(controller.getGroupManager().getSortBy());
sortChooser.sortOrderProperty().addListener(queryInvalidationListener);
getItems().add(2, sortChooser);
sortHelpImageView.setCursor(Cursor.HAND);
sortHelpImageView.setOnMouseClicked(clicked -> {
Text text = new Text(Bundle.Toolbar_sortHelp());
text.setWrappingWidth(480); //This is a hack to fix the layout.
showPopoverHelp(sortHelpImageView,
Bundle.Toolbar_sortHelpTitle(),
sortHelpImageView.getImage(), text);
});
initTagMenuButton();
CategorizeGroupAction cat5GroupAction = new CategorizeGroupAction(DhsImageCategory.FIVE, controller); CategorizeGroupAction cat5GroupAction = new CategorizeGroupAction(DhsImageCategory.FIVE, controller);
catGroupMenuButton.setOnAction(cat5GroupAction); catGroupMenuButton.setOnAction(cat5GroupAction);
catGroupMenuButton.setText(cat5GroupAction.getText()); catGroupMenuButton.setText(cat5GroupAction.getText());
@ -173,41 +224,113 @@ public class Toolbar extends ToolBar {
} }
}); });
groupByLabel.setText(Bundle.Toolbar_groupByLabel()); }
tagImageViewLabel.setText(Bundle.Toolbar_tagImageViewLabel());
categoryImageViewLabel.setText(Bundle.Toolbar_categoryImageViewLabel());
thumbnailSizeLabel.setText(Bundle.Toolbar_thumbnailSizeLabel());
groupByBox.setItems(FXCollections.observableList(DrawableAttribute.getGroupableAttrs())); private DataSource getSelectedDataSource() {
groupByBox.getSelectionModel().select(DrawableAttribute.PATH); Optional<DataSource> empty = Optional.empty();
groupByBox.getSelectionModel().selectedItemProperty().addListener(queryInvalidationListener); return defaultIfNull(dataSourceSelectionModel.getSelectedItem(), empty).orElse(null);
groupByBox.disableProperty().bind(ImageGalleryController.getDefault().regroupDisabled()); }
groupByBox.setCellFactory(listView -> new AttributeListCell());
groupByBox.setButtonCell(new AttributeListCell());
sortChooser = new SortChooser<>(GroupSortBy.getValues()); private void initDataSourceComboBox() {
sortChooser.comparatorProperty().addListener((observable, oldComparator, newComparator) -> { dataSourceComboBox.setCellFactory(param -> new DataSourceCell());
final boolean orderDisabled = newComparator == GroupSortBy.NONE || newComparator == GroupSortBy.PRIORITY; dataSourceComboBox.setButtonCell(new DataSourceCell());
sortChooser.setSortOrderDisabled(orderDisabled); dataSourceComboBox.setConverter(new StringConverter<Optional<DataSource>>() {
@Override
public String toString(Optional<DataSource> object) {
return object.map(DataSource::getName).orElse("All");
}
final SortChooser.ValueType valueType = newComparator == GroupSortBy.GROUP_BY_VALUE ? SortChooser.ValueType.LEXICOGRAPHIC : SortChooser.ValueType.NUMERIC; @Override
sortChooser.setValueType(valueType); public Optional<DataSource> fromString(String string) {
queryInvalidationListener.invalidated(observable); throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
}); });
dataSourceComboBox.setItems(dataSources);
sortChooser.sortOrderProperty().addListener(queryInvalidationListener); Case.addEventTypeSubscriber(ImmutableSet.of(DATA_SOURCE_ADDED, DATA_SOURCE_DELETED),
sortChooser.setComparator(controller.getGroupManager().getSortBy()); evt -> {
getItems().add(1, sortChooser); Platform.runLater(() -> {
sortHelpImageView.setCursor(Cursor.HAND); Optional<DataSource> selectedItem = dataSourceSelectionModel.getSelectedItem();
syncDataSources().addListener(() -> dataSourceSelectionModel.select(selectedItem), Platform::runLater);
sortHelpImageView.setOnMouseClicked(clicked -> {
Text text = new Text(Bundle.Toolbar_sortHelp());
text.setWrappingWidth(480); //This is a hack to fix the layout.
showPopoverHelp(sortHelpImageView,
Bundle.Toolbar_sortHelpTitle(),
sortHelpImageView.getImage(), text);
}); });
});
syncDataSources();
controller.getGroupManager().getDataSourceProperty()
.addListener((observable, oldDataSource, newDataSource)
-> dataSourceSelectionModel.select(Optional.ofNullable(newDataSource)));
dataSourceSelectionModel.select(Optional.ofNullable(controller.getGroupManager().getDataSource()));
dataSourceComboBox.disableProperty().bind(groupByBox.getSelectionModel().selectedItemProperty().isNotEqualTo(DrawableAttribute.PATH));
dataSourceSelectionModel.selectedItemProperty().addListener(queryInvalidationListener);
}
private void initTagMenuButton() {
ListenableFuture<TagGroupAction> future = exec.submit(() -> new TagGroupAction(controller.getTagsManager().getFollowUpTagName(), controller));
Futures.addCallback(future, new FutureCallback<TagGroupAction>() {
@Override
public void onSuccess(TagGroupAction followUpGroupAction) {
tagGroupMenuButton.setOnAction(followUpGroupAction);
tagGroupMenuButton.setText(followUpGroupAction.getText());
tagGroupMenuButton.setGraphic(followUpGroupAction.getGraphic());
}
@Override
public void onFailure(Throwable throwable) {
/*
* The problem appears to be a timing issue where a case is
* closed before this initialization is completed, which It
* appears to be harmless, so we are temporarily changing this
* log message to a WARNING.
*
* TODO (JIRA-3010): SEVERE error logged by image Gallery UI
*/
if (Case.isCaseOpen()) {
logger.log(Level.WARNING, "Could not create Follow Up tag menu item", throwable); //NON-NLS
} else {
// don't add stack trace to log because it makes looking for real errors harder
logger.log(Level.INFO, "Unable to get tag name. Case is closed."); //NON-NLS
}
}
}, Platform::runLater);
tagGroupMenuButton.showingProperty().addListener(showing -> {
if (tagGroupMenuButton.isShowing()) {
ListenableFuture<List<MenuItem>> getTagsFuture = exec.submit(() -> {
return Lists.transform(controller.getTagsManager().getNonCategoryTagNames(),
tagName -> GuiUtils.createAutoAssigningMenuItem(tagGroupMenuButton, new TagGroupAction(tagName, controller)));
});
Futures.addCallback(getTagsFuture, new FutureCallback<List<MenuItem>>() {
@Override
public void onSuccess(List<MenuItem> result) {
tagGroupMenuButton.getItems().setAll(result);
}
@Override
public void onFailure(Throwable t) {
logger.log(Level.SEVERE, "Error getting non-gategory tag names.", t);
}
}, Platform::runLater);
}
});
}
@ThreadConfined(type = ThreadConfined.ThreadType.ANY)
private ListenableFuture<List<DataSource>> syncDataSources() {
ListenableFuture<List<DataSource>> future = exec.submit(controller.getSleuthKitCase()::getDataSources);
Futures.addCallback(future, new FutureCallback<List<DataSource>>() {
@Override
public void onSuccess(List<DataSource> result) {
dataSources.setAll(Collections.singleton(Optional.empty()));
result.forEach(dataSource -> dataSources.add(Optional.of(dataSource)));
}
@Override
public void onFailure(Throwable t) {
logger.log(Level.SEVERE, "Unable to get datasources for current case.", t); //NON-NLS
}
}, Platform::runLater);
return future;
} }
/** /**
@ -237,13 +360,20 @@ public class Toolbar extends ToolBar {
popOver.show(owner); popOver.show(owner);
} }
/**
* Disable the tag and catagory controls if and only if there is no group
* selected.
*
* @param newViewState The GroupViewState to use as a source of the
* selection.
*/
private void syncGroupControlsEnabledState(GroupViewState newViewState) { private void syncGroupControlsEnabledState(GroupViewState newViewState) {
boolean noGroupSelected = newViewState == null boolean noGroupSelected = (null == newViewState)
? true || (null == newViewState.getGroup());
: newViewState.getGroup() == null; Platform.runLater(() -> {
tagGroupMenuButton.setDisable(noGroupSelected); tagGroupMenuButton.setDisable(noGroupSelected);
catGroupMenuButton.setDisable(noGroupSelected); catGroupMenuButton.setDisable(noGroupSelected);
});
} }
public void reset() { public void reset() {
@ -256,5 +386,22 @@ public class Toolbar extends ToolBar {
public Toolbar(ImageGalleryController controller) { public Toolbar(ImageGalleryController controller) {
this.controller = controller; this.controller = controller;
FXMLConstructor.construct(this, "Toolbar.fxml"); //NON-NLS FXMLConstructor.construct(this, "Toolbar.fxml"); //NON-NLS
}
/**
* Cell used to represent a DataSource in the dataSourceComboBoc
*/
static private class DataSourceCell extends ListCell<Optional<DataSource>> {
@Override
protected void updateItem(Optional<DataSource> item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText("All");
} else {
setText(item.map(DataSource::getName).orElse("All"));
}
}
} }
} }

View File

@ -106,7 +106,7 @@ public class DrawableTile extends DrawableTileBase {
@Override @Override
Task<Image> newReadImageTask(DrawableFile file) { Task<Image> newReadImageTask(DrawableFile file) {
return file.getThumbnailTask(); return getController().getThumbsCache().getThumbnailTask(file);
} }
@Override @Override

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2013-2017 Basis Technology Corp. * Copyright 2013-2018 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");
@ -79,9 +79,9 @@ import org.sleuthkit.datamodel.TagName;
import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskCoreException;
/** /**
* An abstract base class for {@link DrawableTile} and {@link SlideShowView}, * An abstract base class for DrawableTile and SlideShowView, since they share a
* since they share a similar node tree and many behaviors, other implementors * similar node tree and many behaviors, other implementors of DrawableViews
* of {@link DrawableView}s should implement the interface directly * should implement the interface directly
* *
* *
* TODO: refactor ExternalViewerAction to supply its own name * TODO: refactor ExternalViewerAction to supply its own name
@ -89,7 +89,7 @@ import org.sleuthkit.datamodel.TskCoreException;
@NbBundle.Messages({"DrawableTileBase.externalViewerAction.text=Open in External Viewer"}) @NbBundle.Messages({"DrawableTileBase.externalViewerAction.text=Open in External Viewer"})
public abstract class DrawableTileBase extends DrawableUIBase { public abstract class DrawableTileBase extends DrawableUIBase {
private static final Logger LOGGER = Logger.getLogger(DrawableTileBase.class.getName()); private static final Logger logger = Logger.getLogger(DrawableTileBase.class.getName());
private static final Border UNSELECTED_BORDER = new Border(new BorderStroke(Color.GRAY, BorderStrokeStyle.SOLID, new CornerRadii(2), new BorderWidths(3))); private static final Border UNSELECTED_BORDER = new Border(new BorderStroke(Color.GRAY, BorderStrokeStyle.SOLID, new CornerRadii(2), new BorderWidths(3)));
private static final Border SELECTED_BORDER = new Border(new BorderStroke(Color.BLUE, BorderStrokeStyle.SOLID, new CornerRadii(2), new BorderWidths(3))); private static final Border SELECTED_BORDER = new Border(new BorderStroke(Color.BLUE, BorderStrokeStyle.SOLID, new CornerRadii(2), new BorderWidths(3)));
@ -187,7 +187,12 @@ public abstract class DrawableTileBase extends DrawableUIBase {
final ArrayList<MenuItem> menuItems = new ArrayList<>(); final ArrayList<MenuItem> menuItems = new ArrayList<>();
menuItems.add(CategorizeAction.getCategoriesMenu(getController())); menuItems.add(CategorizeAction.getCategoriesMenu(getController()));
try {
menuItems.add(AddTagAction.getTagMenu(getController())); menuItems.add(AddTagAction.getTagMenu(getController()));
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Error building tagging context menu.", ex);
}
final Collection<AbstractFile> selectedFilesList = new HashSet<>(Utilities.actionsGlobalContext().lookupAll(AbstractFile.class)); final Collection<AbstractFile> selectedFilesList = new HashSet<>(Utilities.actionsGlobalContext().lookupAll(AbstractFile.class));
if (selectedFilesList.size() == 1) { if (selectedFilesList.size() == 1) {
@ -195,20 +200,19 @@ public abstract class DrawableTileBase extends DrawableUIBase {
} }
final MenuItem extractMenuItem = new MenuItem(Bundle.DrawableTileBase_menuItem_extractFiles()); final MenuItem extractMenuItem = new MenuItem(Bundle.DrawableTileBase_menuItem_extractFiles());
extractMenuItem.setOnAction(actionEvent -> { extractMenuItem.setOnAction(actionEvent
SwingUtilities.invokeLater(() -> { -> SwingUtilities.invokeLater(() -> {
TopComponent etc = WindowManager.getDefault().findTopComponent(ImageGalleryTopComponent.PREFERRED_ID); TopComponent etc = WindowManager.getDefault().findTopComponent(ImageGalleryTopComponent.PREFERRED_ID);
ExtractAction.getInstance().actionPerformed(new java.awt.event.ActionEvent(etc, 0, null)); ExtractAction.getInstance().actionPerformed(new java.awt.event.ActionEvent(etc, 0, null));
}); }));
});
menuItems.add(extractMenuItem); menuItems.add(extractMenuItem);
MenuItem contentViewer = new MenuItem(Bundle.DrawableTileBase_menuItem_showContentViewer()); MenuItem contentViewer = new MenuItem(Bundle.DrawableTileBase_menuItem_showContentViewer());
contentViewer.setOnAction(actionEvent -> { contentViewer.setOnAction(actionEvent
SwingUtilities.invokeLater(() -> { -> SwingUtilities.invokeLater(() -> {
new NewWindowViewAction(Bundle.DrawableTileBase_menuItem_showContentViewer(), new FileNode(file.getAbstractFile())).actionPerformed(null); new NewWindowViewAction(Bundle.DrawableTileBase_menuItem_showContentViewer(), new FileNode(file.getAbstractFile()))
}); .actionPerformed(null);
}); }));
menuItems.add(contentViewer); menuItems.add(contentViewer);
OpenExternalViewerAction openExternalViewerAction = new OpenExternalViewerAction(file); OpenExternalViewerAction openExternalViewerAction = new OpenExternalViewerAction(file);
@ -243,36 +247,33 @@ public abstract class DrawableTileBase extends DrawableUIBase {
protected abstract String getTextForLabel(); protected abstract String getTextForLabel();
protected void initialize() { protected void initialize() {
followUpToggle.setOnAction(actionEvent -> {
getFile().ifPresent(file -> { followUpToggle.setOnAction(
actionEvent -> getFile().ifPresent(
file -> {
if (followUpToggle.isSelected() == true) { if (followUpToggle.isSelected() == true) {
try {
selectionModel.clearAndSelect(file.getId()); selectionModel.clearAndSelect(file.getId());
new AddTagAction(getController(), getController().getTagsManager().getFollowUpTagName(), selectionModel.getSelected()).handle(actionEvent); new AddTagAction(getController(), getController().getTagsManager().getFollowUpTagName(), selectionModel.getSelected()).handle(actionEvent);
} catch (TskCoreException ex) {
LOGGER.log(Level.SEVERE, "Failed to add Follow Up tag. Could not load TagName.", ex); //NON-NLS
}
} else { } else {
new DeleteFollowUpTagAction(getController(), file).handle(actionEvent); new DeleteFollowUpTagAction(getController(), file).handle(actionEvent);
} }
}); })
}); );
} }
protected boolean hasFollowUp() { protected boolean hasFollowUp() {
if (getFileID().isPresent()) { if (getFileID().isPresent()) {
try {
TagName followUpTagName = getController().getTagsManager().getFollowUpTagName(); TagName followUpTagName = getController().getTagsManager().getFollowUpTagName();
if (getFile().isPresent()) {
return DrawableAttribute.TAGS.getValue(getFile().get()).stream() return DrawableAttribute.TAGS.getValue(getFile().get()).stream()
.anyMatch(followUpTagName::equals); .anyMatch(followUpTagName::equals);
} catch (TskCoreException ex) {
LOGGER.log(Level.WARNING, "failed to get follow up tag name ", ex); //NON-NLS
return true;
}
} else { } else {
return false; return false;
} }
} }
return false;
}
@Override @Override
synchronized protected void setFileHelper(final Long newFileID) { synchronized protected void setFileHelper(final Long newFileID) {
@ -342,8 +343,7 @@ public abstract class DrawableTileBase extends DrawableUIBase {
@Override @Override
public void handleTagAdded(ContentTagAddedEvent evt) { public void handleTagAdded(ContentTagAddedEvent evt) {
getFileID().ifPresent(fileID -> { getFileID().ifPresent(fileID -> {
try { final TagName followUpTagName = getController().getTagsManager().getFollowUpTagName(); //NON-NLS
final TagName followUpTagName = getController().getTagsManager().getFollowUpTagName();
final ContentTag addedTag = evt.getAddedTag(); final ContentTag addedTag = evt.getAddedTag();
if (fileID == addedTag.getContent().getId() if (fileID == addedTag.getContent().getId()
&& addedTag.getName().equals(followUpTagName)) { && addedTag.getName().equals(followUpTagName)) {
@ -352,9 +352,6 @@ public abstract class DrawableTileBase extends DrawableUIBase {
followUpToggle.setSelected(true); followUpToggle.setSelected(true);
}); });
} }
} catch (TskCoreException ex) {
LOGGER.log(Level.SEVERE, "Failed to get followup tag name. Unable to update follow up status for file. ", ex); //NON-NLS
}
}); });
} }
@ -362,16 +359,12 @@ public abstract class DrawableTileBase extends DrawableUIBase {
@Override @Override
public void handleTagDeleted(ContentTagDeletedEvent evt) { public void handleTagDeleted(ContentTagDeletedEvent evt) {
getFileID().ifPresent(fileID -> { getFileID().ifPresent(fileID -> {
try { final TagName followUpTagName = getController().getTagsManager().getFollowUpTagName(); //NON-NLS
final TagName followUpTagName = getController().getTagsManager().getFollowUpTagName();
final ContentTagDeletedEvent.DeletedContentTagInfo deletedTagInfo = evt.getDeletedTagInfo(); final ContentTagDeletedEvent.DeletedContentTagInfo deletedTagInfo = evt.getDeletedTagInfo();
if (fileID == deletedTagInfo.getContentID() if (fileID == deletedTagInfo.getContentID()
&& deletedTagInfo.getName().equals(followUpTagName)) { && deletedTagInfo.getName().equals(followUpTagName)) {
updateFollowUpIcon(); updateFollowUpIcon();
} }
} catch (TskCoreException ex) {
LOGGER.log(Level.SEVERE, "Failed to get followup tag name. Unable to update follow up status for file. ", ex); //NON-NLS
}
}); });
} }

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2015 Basis Technology Corp. * Copyright 2015-18 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");
@ -25,7 +25,6 @@ import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.logging.Level;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import javafx.fxml.FXML; import javafx.fxml.FXML;
@ -41,7 +40,6 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import org.controlsfx.control.action.ActionUtils; import org.controlsfx.control.action.ActionUtils;
import org.openide.util.NbBundle; import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.actions.OpenExternalViewerAction; import org.sleuthkit.autopsy.imagegallery.actions.OpenExternalViewerAction;
@ -55,10 +53,10 @@ import org.sleuthkit.datamodel.TskCoreException;
"DrawableUIBase.errorLabel.OOMText=Insufficent memory"}) "DrawableUIBase.errorLabel.OOMText=Insufficent memory"})
abstract public class DrawableUIBase extends AnchorPane implements DrawableView { abstract public class DrawableUIBase extends AnchorPane implements DrawableView {
/** The use of SingleThreadExecutor means we can only load a single image at
* a time */
static final Executor exec = Executors.newSingleThreadExecutor(); static final Executor exec = Executors.newSingleThreadExecutor();
private static final Logger LOGGER = Logger.getLogger(DrawableUIBase.class.getName());
@FXML @FXML
BorderPane imageBorder; BorderPane imageBorder;
@FXML @FXML
@ -96,21 +94,18 @@ abstract public class DrawableUIBase extends AnchorPane implements DrawableView
@Override @Override
synchronized public Optional<DrawableFile> getFile() { synchronized public Optional<DrawableFile> getFile() {
if (fileIDOpt.isPresent()) { if (fileIDOpt.isPresent()) {
if (fileOpt.isPresent() && fileOpt.get().getId() == fileIDOpt.get()) { if (!fileOpt.isPresent() || fileOpt.get().getId() != fileIDOpt.get()) {
return fileOpt;
} else {
try { try {
fileOpt = Optional.ofNullable(getController().getFileFromId(fileIDOpt.get())); fileOpt = Optional.ofNullable(getController().getFileFromID(fileIDOpt.get()));
} catch (TskCoreException ex) { } catch (TskCoreException ex) {
Logger.getAnonymousLogger().log(Level.WARNING, "failed to get DrawableFile for obj_id" + fileIDOpt.get(), ex); //NON-NLS fileOpt = Optional.empty();
}
}
} else {
fileOpt = Optional.empty(); fileOpt = Optional.empty();
} }
return fileOpt; return fileOpt;
} }
} else {
return Optional.empty();
}
}
protected abstract void setFileHelper(Long newFileID); protected abstract void setFileHelper(Long newFileID);
@ -126,9 +121,7 @@ abstract public class DrawableUIBase extends AnchorPane implements DrawableView
} }
synchronized protected void updateContent() { synchronized protected void updateContent() {
if (getFile().isPresent()) { getFile().ifPresent(this::doReadImageTask);
doReadImageTask(getFile().get());
}
} }
synchronized Node doReadImageTask(DrawableFile file) { synchronized Node doReadImageTask(DrawableFile file) {
@ -138,13 +131,13 @@ abstract public class DrawableUIBase extends AnchorPane implements DrawableView
Platform.runLater(() -> imageBorder.setCenter(progressNode)); Platform.runLater(() -> imageBorder.setCenter(progressNode));
//called on fx thread //called on fx thread
myTask.setOnSucceeded(succeeded -> { myTask.setOnSucceeded(succeeded -> { //on fx thread
showImage(file, myTask); showImage(file, myTask);
synchronized (DrawableUIBase.this) { synchronized (DrawableUIBase.this) {
imageTask = null; imageTask = null;
} }
}); });
myTask.setOnFailed(failed -> { myTask.setOnFailed(failed -> { //on fx thread
Throwable exception = myTask.getException(); Throwable exception = myTask.getException();
if (exception instanceof OutOfMemoryError if (exception instanceof OutOfMemoryError
&& exception.getMessage().contains("Java heap space")) { //NON-NLS && exception.getMessage().contains("Java heap space")) { //NON-NLS
@ -156,7 +149,7 @@ abstract public class DrawableUIBase extends AnchorPane implements DrawableView
imageTask = null; imageTask = null;
} }
}); });
myTask.setOnCancelled(cancelled -> { myTask.setOnCancelled(cancelled -> { //on fx thread
synchronized (DrawableUIBase.this) { synchronized (DrawableUIBase.this) {
imageTask = null; imageTask = null;
} }
@ -180,9 +173,12 @@ abstract public class DrawableUIBase extends AnchorPane implements DrawableView
} }
/** /**
* Get a new progress indicator to use as a place holder for the image in
* this view.
* *
* @param file the value of file * @param imageTask The imageTask to get a progress indicator for.
* @param imageTask the value of imageTask *
* @return The new Node to use as a progress indicator.
*/ */
Node newProgressIndicator(final Task<?> imageTask) { Node newProgressIndicator(final Task<?> imageTask) {
ProgressIndicator loadingProgressIndicator = new ProgressIndicator(-1); ProgressIndicator loadingProgressIndicator = new ProgressIndicator(-1);

View File

@ -1,3 +1,21 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2015-18 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.drawableviews; package org.sleuthkit.autopsy.imagegallery.gui.drawableviews;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
@ -16,8 +34,8 @@ import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent;
import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory; import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
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.DrawableFile; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile;
@ -60,9 +78,8 @@ public interface DrawableView {
/** /**
* update the visual representation of the category of the assigned file. * update the visual representation of the category of the assigned file.
* Implementations of {@link DrawableView} must register themselves with * Implementations of DrawableView } must register themselves with
* {@link CategoryManager#registerListener(java.lang.Object)} to ahve this * CategoryManager.registerListener()} to have this method invoked
* method invoked
* *
* @param evt the CategoryChangeEvent to handle * @param evt the CategoryChangeEvent to handle
*/ */
@ -95,6 +112,7 @@ public interface DrawableView {
Logger.getLogger(DrawableView.class.getName()).log(Level.WARNING, "Error looking up hash set hits"); //NON-NLS Logger.getLogger(DrawableView.class.getName()).log(Level.WARNING, "Error looking up hash set hits"); //NON-NLS
return false; return false;
} }
} }
static Border getCategoryBorder(DhsImageCategory category) { static Border getCategoryBorder(DhsImageCategory category) {

View File

@ -2,6 +2,7 @@
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?> <?import javafx.scene.control.Button?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Label?> <?import javafx.scene.control.Label?>
<?import javafx.scene.control.MenuItem?> <?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.RadioButton?> <?import javafx.scene.control.RadioButton?>
@ -11,6 +12,7 @@
<?import javafx.scene.control.ToolBar?> <?import javafx.scene.control.ToolBar?>
<?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.BorderPane?> <?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?> <?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.VBox?>
@ -18,7 +20,7 @@
<?import org.controlsfx.control.GridView?> <?import org.controlsfx.control.GridView?>
<?import org.controlsfx.control.SegmentedButton?> <?import org.controlsfx.control.SegmentedButton?>
<fx:root type="BorderPane" xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1"> <fx:root type="BorderPane" xmlns="http://javafx.com/javafx/8.0.141" xmlns:fx="http://javafx.com/fxml/1">
<center> <center>
<GridView fx:id="gridView" BorderPane.alignment="CENTER" /> <GridView fx:id="gridView" BorderPane.alignment="CENTER" />
@ -29,7 +31,7 @@
<HBox alignment="CENTER_LEFT" spacing="5.0" BorderPane.alignment="TOP_LEFT"> <HBox alignment="CENTER_LEFT" spacing="5.0" BorderPane.alignment="TOP_LEFT">
<children> <children>
<Label fx:id="bottomLabel" text="Group Viewing History: " /> <Label fx:id="bottomLabel" text="Group Viewing History: " />
<Button fx:id="backButton" mnemonicParsing="false" text="back"> <Button fx:id="backButton" mnemonicParsing="false" text="Back">
<graphic> <graphic>
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true"> <ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
<image> <image>
@ -38,7 +40,7 @@
</ImageView> </ImageView>
</graphic> </graphic>
</Button> </Button>
<Button fx:id="forwardButton" mnemonicParsing="false" text="forward"> <Button fx:id="forwardButton" mnemonicParsing="false" text="Forward">
<graphic> <graphic>
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true"> <ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
<image> <image>
@ -56,10 +58,13 @@
<right> <right>
<HBox alignment="CENTER_RIGHT" spacing="5.0" BorderPane.alignment="TOP_RIGHT"> <HBox alignment="CENTER_RIGHT" spacing="5.0" BorderPane.alignment="TOP_RIGHT">
<children> <children>
<Button fx:id="nextButton" contentDisplay="RIGHT" mnemonicParsing="false" text="next unseen group" BorderPane.alignment="CENTER_RIGHT"> <CheckBox fx:id="seenByOtherExaminersCheckBox" mnemonicParsing="false" text="Don't show groups seen by other examiners" />
<AnchorPane fx:id="nextButtonPane" BorderPane.alignment="CENTER_RIGHT">
<BorderPane.margin> <BorderPane.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</BorderPane.margin> </BorderPane.margin>
<children>
<Button fx:id="nextButton" contentDisplay="RIGHT" minWidth="175.0" mnemonicParsing="false" text="All Groups Gave Been Seen">
<graphic> <graphic>
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true"> <ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
<image> <image>
@ -69,6 +74,8 @@
</graphic> </graphic>
</Button> </Button>
</children> </children>
</AnchorPane>
</children>
<BorderPane.margin> <BorderPane.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</BorderPane.margin> </BorderPane.margin>

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2013-16 Basis Technology Corp. * Copyright 2013-18 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");
@ -21,6 +21,10 @@ package org.sleuthkit.autopsy.imagegallery.gui.drawableviews;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
@ -53,7 +57,9 @@ import javafx.event.ActionEvent;
import javafx.event.EventHandler; import javafx.event.EventHandler;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.geometry.Bounds; import javafx.geometry.Bounds;
import javafx.scene.Cursor;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContextMenu; import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.MenuItem; import javafx.scene.control.MenuItem;
@ -81,6 +87,7 @@ import static javafx.scene.input.KeyCode.RIGHT;
import static javafx.scene.input.KeyCode.UP; import static javafx.scene.input.KeyCode.UP;
import javafx.scene.input.KeyEvent; import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent; import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Border; import javafx.scene.layout.Border;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.BorderStroke; import javafx.scene.layout.BorderStroke;
@ -90,8 +97,8 @@ import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.util.Duration; import javafx.util.Duration;
import javax.swing.Action;
import javax.swing.SwingUtilities; import javax.swing.SwingUtilities;
import static org.apache.commons.collections4.CollectionUtils.isNotEmpty;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.controlsfx.control.GridCell; import org.controlsfx.control.GridCell;
import org.controlsfx.control.GridView; import org.controlsfx.control.GridView;
@ -107,6 +114,7 @@ import org.sleuthkit.autopsy.corecomponentinterfaces.ContextMenuActionsProvider;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.coreutils.ThreadConfined.ThreadType; import org.sleuthkit.autopsy.coreutils.ThreadConfined.ThreadType;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.directorytree.ExtractAction; import org.sleuthkit.autopsy.directorytree.ExtractAction;
import org.sleuthkit.autopsy.imagegallery.FXMLConstructor; import org.sleuthkit.autopsy.imagegallery.FXMLConstructor;
import org.sleuthkit.autopsy.imagegallery.FileIDSelectionModel; import org.sleuthkit.autopsy.imagegallery.FileIDSelectionModel;
@ -122,18 +130,17 @@ import org.sleuthkit.autopsy.imagegallery.actions.RedoAction;
import org.sleuthkit.autopsy.imagegallery.actions.SwingMenuItemAdapter; import org.sleuthkit.autopsy.imagegallery.actions.SwingMenuItemAdapter;
import org.sleuthkit.autopsy.imagegallery.actions.TagSelectedFilesAction; import org.sleuthkit.autopsy.imagegallery.actions.TagSelectedFilesAction;
import org.sleuthkit.autopsy.imagegallery.actions.UndoAction; import org.sleuthkit.autopsy.imagegallery.actions.UndoAction;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewMode; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewMode;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState;
import org.sleuthkit.autopsy.imagegallery.gui.GuiUtils; import org.sleuthkit.autopsy.imagegallery.gui.GuiUtils;
import org.sleuthkit.autopsy.imagegallery.utils.TaskUtils;
import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskCoreException;
/** /**
* A GroupPane displays the contents of a {@link DrawableGroup}. It supports * A GroupPane displays the contents of a DrawableGroup. It supports both
* both a {@link GridView} based view and a {@link SlideShowView} view by * GridView and SlideShowView modes by swapping out its internal components.
* swapping out its internal components.
* *
* *
* TODO: Extract the The GridView instance to a separate class analogous to the * TODO: Extract the The GridView instance to a separate class analogous to the
@ -145,23 +152,21 @@ import org.sleuthkit.datamodel.TskCoreException;
*/ */
public class GroupPane extends BorderPane { public class GroupPane extends BorderPane {
private static final Logger LOGGER = Logger.getLogger(GroupPane.class.getName()); private static final Logger logger = Logger.getLogger(GroupPane.class.getName());
private static final BorderWidths BORDER_WIDTHS_2 = new BorderWidths(2); private static final BorderWidths BORDER_WIDTHS_2 = new BorderWidths(2);
private static final CornerRadii CORNER_RADII_2 = new CornerRadii(2); private static final CornerRadii CORNER_RADII_2 = new CornerRadii(2);
private static final DropShadow DROP_SHADOW = new DropShadow(10, Color.BLUE); private static final DropShadow DROP_SHADOW = new DropShadow(10, Color.BLUE);
private static final Timeline flashAnimation = new Timeline(new KeyFrame(Duration.millis(400), new KeyValue(DROP_SHADOW.radiusProperty(), 1, Interpolator.LINEAR)), private static final Timeline flashAnimation = new Timeline(
new KeyFrame(Duration.millis(400), new KeyValue(DROP_SHADOW.radiusProperty(), 1, Interpolator.LINEAR)),
new KeyFrame(Duration.millis(400), new KeyValue(DROP_SHADOW.radiusProperty(), 15, Interpolator.LINEAR)) new KeyFrame(Duration.millis(400), new KeyValue(DROP_SHADOW.radiusProperty(), 15, Interpolator.LINEAR))
); );
private final FileIDSelectionModel selectionModel; private static final List<KeyCode> categoryKeyCodes = Arrays.asList(
private static final List<KeyCode> categoryKeyCodes = Arrays.asList(KeyCode.NUMPAD0, KeyCode.NUMPAD1, KeyCode.NUMPAD2, KeyCode.NUMPAD3, KeyCode.NUMPAD4, KeyCode.NUMPAD5, NUMPAD0, NUMPAD1, NUMPAD2, NUMPAD3, NUMPAD4, NUMPAD5,
KeyCode.DIGIT0, KeyCode.DIGIT1, KeyCode.DIGIT2, KeyCode.DIGIT3, KeyCode.DIGIT4, KeyCode.DIGIT5); DIGIT0, DIGIT1, DIGIT2, DIGIT3, DIGIT4, DIGIT5);
private final Back backAction;
private final Forward forwardAction;
@FXML @FXML
private Button undoButton; private Button undoButton;
@ -170,13 +175,10 @@ public class GroupPane extends BorderPane {
@FXML @FXML
private SplitMenuButton catSelectedSplitMenu; private SplitMenuButton catSelectedSplitMenu;
@FXML @FXML
private SplitMenuButton tagSelectedSplitMenu; private SplitMenuButton tagSelectedSplitMenu;
@FXML @FXML
private ToolBar headerToolBar; private ToolBar headerToolBar;
@FXML @FXML
private ToggleButton cat0Toggle; private ToggleButton cat0Toggle;
@FXML @FXML
@ -193,26 +195,25 @@ public class GroupPane extends BorderPane {
@FXML @FXML
private SegmentedButton segButton; private SegmentedButton segButton;
private SlideShowView slideShowPane;
@FXML @FXML
private ToggleButton slideShowToggle; private ToggleButton slideShowToggle;
@FXML
private GridView<Long> gridView;
@FXML @FXML
private ToggleButton tileToggle; private ToggleButton tileToggle;
private SlideShowView slideShowPane;
@FXML
private GridView<Long> gridView;
@FXML @FXML
private Button nextButton; private Button nextButton;
@FXML
private AnchorPane nextButtonPane;
@FXML
private CheckBox seenByOtherExaminersCheckBox;
@FXML @FXML
private Button backButton; private Button backButton;
@FXML @FXML
private Button forwardButton; private Button forwardButton;
@FXML @FXML
private Label groupLabel; private Label groupLabel;
@FXML @FXML
@ -229,30 +230,27 @@ public class GroupPane extends BorderPane {
@FXML @FXML
private HBox catSplitMenuContainer; private HBox catSplitMenuContainer;
private final KeyboardHandler tileKeyboardNavigationHandler = new KeyboardHandler(); private final ListeningExecutorService exec = TaskUtils.getExecutorForClass(GroupPane.class);
private final NextUnseenGroup nextGroupAction;
private final ImageGalleryController controller; private final ImageGalleryController controller;
private ContextMenu contextMenu; private final FileIDSelectionModel selectionModel;
private Integer selectionAnchorIndex; private Integer selectionAnchorIndex;
private final UndoAction undoAction; private final UndoAction undoAction;
private final RedoAction redoAction; private final RedoAction redoAction;
private final Back backAction;
private final Forward forwardAction;
private final NextUnseenGroup nextGroupAction;
GroupViewMode getGroupViewMode() { private final KeyboardHandler tileKeyboardNavigationHandler = new KeyboardHandler();
return groupViewMode.get();
}
/** private ContextMenu contextMenu;
* the current GroupViewMode of this GroupPane
*/ /** the current GroupViewMode of this GroupPane */
private final SimpleObjectProperty<GroupViewMode> groupViewMode = new SimpleObjectProperty<>(GroupViewMode.TILE); private final SimpleObjectProperty<GroupViewMode> groupViewMode = new SimpleObjectProperty<>(GroupViewMode.TILE);
/** /** the grouping this pane is currently the view for */
* the grouping this pane is currently the view for
*/
private final ReadOnlyObjectWrapper<DrawableGroup> grouping = new ReadOnlyObjectWrapper<>(); private final ReadOnlyObjectWrapper<DrawableGroup> grouping = new ReadOnlyObjectWrapper<>();
/** /**
@ -286,6 +284,10 @@ public class GroupPane extends BorderPane {
FXMLConstructor.construct(this, "GroupPane.fxml"); //NON-NLS FXMLConstructor.construct(this, "GroupPane.fxml"); //NON-NLS
} }
GroupViewMode getGroupViewMode() {
return groupViewMode.get();
}
@ThreadConfined(type = ThreadType.JFX) @ThreadConfined(type = ThreadType.JFX)
public void activateSlideShowViewer(Long slideShowFileID) { public void activateSlideShowViewer(Long slideShowFileID) {
groupViewMode.set(GroupViewMode.SLIDE_SHOW); groupViewMode.set(GroupViewMode.SLIDE_SHOW);
@ -332,7 +334,9 @@ public class GroupPane extends BorderPane {
} }
/** /**
* create the string to display in the group header * Create the string to display in the group header.
*
* @return The string to display in the group header.
*/ */
@NbBundle.Messages({"# {0} - default group name", @NbBundle.Messages({"# {0} - default group name",
"# {1} - hashset hits count", "# {1} - hashset hits count",
@ -383,19 +387,20 @@ public class GroupPane extends BorderPane {
"GroupPane.catContainerLabel.displayText=Categorize Selected File:", "GroupPane.catContainerLabel.displayText=Categorize Selected File:",
"GroupPane.catHeadingLabel.displayText=Category:"}) "GroupPane.catHeadingLabel.displayText=Category:"})
void initialize() { void initialize() {
assert cat0Toggle != null : "fx:id=\"cat0Toggle\" was not injected: check your FXML file 'SlideShowView.fxml'."; assert cat0Toggle != null : "fx:id=\"cat0Toggle\" was not injected: check your FXML file 'GroupPane.fxml'.";
assert cat1Toggle != null : "fx:id=\"cat1Toggle\" was not injected: check your FXML file 'SlideShowView.fxml'."; assert cat1Toggle != null : "fx:id=\"cat1Toggle\" was not injected: check your FXML file 'GroupPane.fxml'.";
assert cat2Toggle != null : "fx:id=\"cat2Toggle\" was not injected: check your FXML file 'SlideShowView.fxml'."; assert cat2Toggle != null : "fx:id=\"cat2Toggle\" was not injected: check your FXML file 'GroupPane.fxml'.";
assert cat3Toggle != null : "fx:id=\"cat3Toggle\" was not injected: check your FXML file 'SlideShowView.fxml'."; assert cat3Toggle != null : "fx:id=\"cat3Toggle\" was not injected: check your FXML file 'GroupPane.fxml'.";
assert cat4Toggle != null : "fx:id=\"cat4Toggle\" was not injected: check your FXML file 'SlideShowView.fxml'."; assert cat4Toggle != null : "fx:id=\"cat4Toggle\" was not injected: check your FXML file 'GroupPane.fxml'.";
assert cat5Toggle != null : "fx:id=\"cat5Toggle\" was not injected: check your FXML file 'SlideShowView.fxml'."; assert cat5Toggle != null : "fx:id=\"cat5Toggle\" was not injected: check your FXML file 'GroupPane.fxml'.";
assert gridView != null : "fx:id=\"tilePane\" was not injected: check your FXML file 'GroupPane.fxml'."; assert gridView != null : "fx:id=\"tilePane\" was not injected: check your FXML file 'GroupPane.fxml'.";
assert catSelectedSplitMenu != null : "fx:id=\"grpCatSplitMenu\" was not injected: check your FXML file 'GroupHeader.fxml'."; assert catSelectedSplitMenu != null : "fx:id=\"grpCatSplitMenu\" was not injected: check your FXML file 'GroupPane.fxml'.";
assert tagSelectedSplitMenu != null : "fx:id=\"grpTagSplitMenu\" was not injected: check your FXML file 'GroupHeader.fxml'."; assert tagSelectedSplitMenu != null : "fx:id=\"grpTagSplitMenu\" was not injected: check your FXML file 'GroupPane.fxml'.";
assert headerToolBar != null : "fx:id=\"headerToolBar\" was not injected: check your FXML file 'GroupHeader.fxml'."; assert headerToolBar != null : "fx:id=\"headerToolBar\" was not injected: check your FXML file 'GroupPane.fxml'.";
assert segButton != null : "fx:id=\"previewList\" was not injected: check your FXML file 'GroupHeader.fxml'."; assert segButton != null : "fx:id=\"previewList\" was not injected: check your FXML file 'GroupPane.fxml'.";
assert slideShowToggle != null : "fx:id=\"segButton\" was not injected: check your FXML file 'GroupHeader.fxml'."; assert slideShowToggle != null : "fx:id=\"segButton\" was not injected: check your FXML file 'GroupPane.fxml'.";
assert tileToggle != null : "fx:id=\"tileToggle\" was not injected: check your FXML file 'GroupHeader.fxml'."; assert tileToggle != null : "fx:id=\"tileToggle\" was not injected: check your FXML file 'GroupPane.fxml'.";
assert seenByOtherExaminersCheckBox != null : "fx:id=\"seenByOtherExaminersCheckBox\" was not injected: check your FXML file 'GroupPane.fxml'.";
for (DhsImageCategory cat : DhsImageCategory.values()) { for (DhsImageCategory cat : DhsImageCategory.values()) {
ToggleButton toggleForCategory = getToggleForCategory(cat); ToggleButton toggleForCategory = getToggleForCategory(cat);
@ -426,27 +431,36 @@ public class GroupPane extends BorderPane {
catSelectedSplitMenu.disableProperty().bind(isSelectionEmpty); catSelectedSplitMenu.disableProperty().bind(isSelectionEmpty);
tagSelectedSplitMenu.disableProperty().bind(isSelectionEmpty); tagSelectedSplitMenu.disableProperty().bind(isSelectionEmpty);
TagSelectedFilesAction followUpSelectedAction = new TagSelectedFilesAction(controller.getTagsManager().getFollowUpTagName(), controller); //NON-NLS
Platform.runLater(() -> { Platform.runLater(() -> {
try { tagSelectedSplitMenu.setText(followUpSelectedAction.getText());
TagSelectedFilesAction followUpSelectedACtion = new TagSelectedFilesAction(controller.getTagsManager().getFollowUpTagName(), controller); tagSelectedSplitMenu.setGraphic(followUpSelectedAction.getGraphic());
tagSelectedSplitMenu.setText(followUpSelectedACtion.getText()); tagSelectedSplitMenu.setOnAction(followUpSelectedAction);
tagSelectedSplitMenu.setGraphic(followUpSelectedACtion.getGraphic());
tagSelectedSplitMenu.setOnAction(followUpSelectedACtion);
} catch (TskCoreException tskCoreException) {
LOGGER.log(Level.WARNING, "failed to load FollowUpTagName", tskCoreException); //NON-NLS
}
tagSelectedSplitMenu.showingProperty().addListener(showing -> { tagSelectedSplitMenu.showingProperty().addListener(showing -> {
if (tagSelectedSplitMenu.isShowing()) { if (tagSelectedSplitMenu.isShowing()) {
List<MenuItem> selTagMenues = Lists.transform(controller.getTagsManager().getNonCategoryTagNames(),
tagName -> GuiUtils.createAutoAssigningMenuItem(tagSelectedSplitMenu, new TagSelectedFilesAction(tagName, controller))); ListenableFuture<List<MenuItem>> getTagsFuture = exec.submit(()
tagSelectedSplitMenu.getItems().setAll(selTagMenues); -> Lists.transform(controller.getTagsManager().getNonCategoryTagNames(),
tagName -> GuiUtils.createAutoAssigningMenuItem(tagSelectedSplitMenu, new TagSelectedFilesAction(tagName, controller))));
Futures.addCallback(getTagsFuture, new FutureCallback<List<MenuItem>>() {
@Override
public void onSuccess(List<MenuItem> result) {
tagSelectedSplitMenu.getItems().setAll(result);
}
@Override
public void onFailure(Throwable throwable) {
logger.log(Level.SEVERE, "Error getting tag names.", throwable);
}
}, Platform::runLater);
} }
}); });
}); });
CategorizeSelectedFilesAction cat5SelectedAction = new CategorizeSelectedFilesAction(DhsImageCategory.FIVE, controller); CategorizeSelectedFilesAction cat5SelectedAction = new CategorizeSelectedFilesAction(DhsImageCategory.FIVE, controller);
catSelectedSplitMenu.setOnAction(cat5SelectedAction); catSelectedSplitMenu.setOnAction(cat5SelectedAction);
catSelectedSplitMenu.setText(cat5SelectedAction.getText()); catSelectedSplitMenu.setText(cat5SelectedAction.getText());
catSelectedSplitMenu.setGraphic(cat5SelectedAction.getGraphic()); catSelectedSplitMenu.setGraphic(cat5SelectedAction.getGraphic());
catSelectedSplitMenu.showingProperty().addListener(showing -> { catSelectedSplitMenu.showingProperty().addListener(showing -> {
@ -482,7 +496,7 @@ public class GroupPane extends BorderPane {
slideShowToggle.setOnAction(onAction -> activateSlideShowViewer(selectionModel.lastSelectedProperty().get())); slideShowToggle.setOnAction(onAction -> activateSlideShowViewer(selectionModel.lastSelectedProperty().get()));
tileToggle.setOnAction(onAction -> activateTileViewer()); tileToggle.setOnAction(onAction -> activateTileViewer());
controller.viewState().addListener((observable, oldViewState, newViewState) -> setViewState(newViewState)); controller.viewStateProperty().addListener((observable, oldViewState, newViewState) -> setViewState(newViewState));
addEventFilter(KeyEvent.KEY_PRESSED, tileKeyboardNavigationHandler); addEventFilter(KeyEvent.KEY_PRESSED, tileKeyboardNavigationHandler);
gridView.addEventHandler(MouseEvent.MOUSE_CLICKED, new MouseHandler()); gridView.addEventHandler(MouseEvent.MOUSE_CLICKED, new MouseHandler());
@ -513,6 +527,16 @@ public class GroupPane extends BorderPane {
} }
}); });
seenByOtherExaminersCheckBox.selectedProperty().addListener((observable, oldValue, newValue) -> {
nextButtonPane.setDisable(true);
nextButtonPane.setCursor(Cursor.WAIT);
exec.submit(() -> controller.getGroupManager().setCollaborativeMode(newValue))
.addListener(() -> {
nextButtonPane.setDisable(false);
nextButtonPane.setCursor(Cursor.DEFAULT);
}, Platform::runLater);
});
//listen to tile selection and make sure it is visible in scroll area //listen to tile selection and make sure it is visible in scroll area
selectionModel.lastSelectedProperty().addListener((observable, oldFileID, newFileId) -> { selectionModel.lastSelectedProperty().addListener((observable, oldFileID, newFileId) -> {
if (groupViewMode.get() == GroupViewMode.SLIDE_SHOW if (groupViewMode.get() == GroupViewMode.SLIDE_SHOW
@ -523,7 +547,7 @@ public class GroupPane extends BorderPane {
} }
}); });
setViewState(controller.viewState().get()); setViewState(controller.viewStateProperty().get());
} }
//TODO: make sure we are testing complete visability not just bounds intersection //TODO: make sure we are testing complete visability not just bounds intersection
@ -590,11 +614,11 @@ public class GroupPane extends BorderPane {
* assigns a grouping for this pane to represent and initializes grouping * assigns a grouping for this pane to represent and initializes grouping
* specific properties and listeners * specific properties and listeners
* *
* @param grouping the new grouping assigned to this group * @param newViewState
*/ */
void setViewState(GroupViewState viewState) { void setViewState(GroupViewState newViewState) {
if (isNull(viewState) || isNull(viewState.getGroup())) { if (isNull(newViewState) || isNull(newViewState.getGroup().orElse(null))) {
if (nonNull(getGroup())) { if (nonNull(getGroup())) {
getGroup().getFileIDs().removeListener(filesSyncListener); getGroup().getFileIDs().removeListener(filesSyncListener);
} }
@ -613,11 +637,11 @@ public class GroupPane extends BorderPane {
}); });
} else { } else {
if (getGroup() != viewState.getGroup()) { if (nonNull(getGroup()) && getGroup() != newViewState.getGroup().get()) {
if (nonNull(getGroup())) {
getGroup().getFileIDs().removeListener(filesSyncListener); getGroup().getFileIDs().removeListener(filesSyncListener);
} }
this.grouping.set(viewState.getGroup());
this.grouping.set(newViewState.getGroup().get());
getGroup().getFileIDs().addListener(filesSyncListener); getGroup().getFileIDs().addListener(filesSyncListener);
@ -628,21 +652,18 @@ public class GroupPane extends BorderPane {
slideShowToggle.setDisable(gridView.getItems().isEmpty()); slideShowToggle.setDisable(gridView.getItems().isEmpty());
groupLabel.setText(header); groupLabel.setText(header);
resetScrollBar(); resetScrollBar();
if (viewState.getMode() == GroupViewMode.TILE) { if (newViewState.getMode() == GroupViewMode.TILE) {
activateTileViewer(); activateTileViewer();
} else { } else {
activateSlideShowViewer(viewState.getSlideShowfileID().orElse(null)); activateSlideShowViewer(newViewState.getSlideShowfileID().orElse(null));
} }
}); });
} }
} }
}
@ThreadConfined(type = ThreadType.JFX) @ThreadConfined(type = ThreadType.JFX)
private void resetScrollBar() { private void resetScrollBar() {
getScrollBar().ifPresent((scrollBar) -> { getScrollBar().ifPresent(scrollBar -> scrollBar.setValue(0));
scrollBar.setValue(0);
});
} }
@ThreadConfined(type = ThreadType.JFX) @ThreadConfined(type = ThreadType.JFX)
@ -667,6 +688,7 @@ public class GroupPane extends BorderPane {
} else { } else {
selectionAnchorIndex = null; selectionAnchorIndex = null;
selectionModel.clearAndSelect(newFileID); selectionModel.clearAndSelect(newFileID);
} }
} }
@ -689,7 +711,6 @@ public class GroupPane extends BorderPane {
} }
} }
cellMap.put(newValue, DrawableCell.this); cellMap.put(newValue, DrawableCell.this);
} }
}); });
@ -832,23 +853,24 @@ public class GroupPane extends BorderPane {
private ContextMenu buildContextMenu() { private ContextMenu buildContextMenu() {
ArrayList<MenuItem> menuItems = new ArrayList<>(); ArrayList<MenuItem> menuItems = new ArrayList<>();
menuItems.add(CategorizeAction.getCategoriesMenu(controller)); menuItems.add(CategorizeAction.getCategoriesMenu(controller));
try {
menuItems.add(AddTagAction.getTagMenu(controller)); menuItems.add(AddTagAction.getTagMenu(controller));
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Error building tagging context menu.", ex);
Collection<? extends ContextMenuActionsProvider> menuProviders = Lookup.getDefault().lookupAll(ContextMenuActionsProvider.class);
for (ContextMenuActionsProvider provider : menuProviders) {
for (final Action act : provider.getActions()) {
if (act instanceof Presenter.Popup) {
Presenter.Popup aact = (Presenter.Popup) act;
menuItems.add(SwingMenuItemAdapter.create(aact.getPopupPresenter()));
}
}
} }
Lookup.getDefault().lookupAll(ContextMenuActionsProvider.class).stream()
.map(ContextMenuActionsProvider::getActions)
.flatMap(Collection::stream)
.filter(Presenter.Popup.class::isInstance)
.map(Presenter.Popup.class::cast)
.map(Presenter.Popup::getPopupPresenter)
.map(SwingMenuItemAdapter::create)
.forEachOrdered(menuItems::add);
final MenuItem extractMenuItem = new MenuItem(Bundle.GroupPane_gridViewContextMenuItem_extractFiles()); final MenuItem extractMenuItem = new MenuItem(Bundle.GroupPane_gridViewContextMenuItem_extractFiles());
extractMenuItem.setOnAction((ActionEvent t) -> {
extractMenuItem.setOnAction(actionEvent -> {
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
TopComponent etc = WindowManager.getDefault().findTopComponent(ImageGalleryTopComponent.PREFERRED_ID); TopComponent etc = WindowManager.getDefault().findTopComponent(ImageGalleryTopComponent.PREFERRED_ID);
ExtractAction.getInstance().actionPerformed(new java.awt.event.ActionEvent(etc, 0, null)); ExtractAction.getInstance().actionPerformed(new java.awt.event.ActionEvent(etc, 0, null));
@ -857,7 +879,9 @@ public class GroupPane extends BorderPane {
menuItems.add(extractMenuItem); menuItems.add(extractMenuItem);
ContextMenu contextMenu = new ContextMenu(menuItems.toArray(new MenuItem[]{})); ContextMenu contextMenu = new ContextMenu(menuItems.toArray(new MenuItem[]{}));
contextMenu.setAutoHide(true);
contextMenu.setAutoHide(
true);
return contextMenu; return contextMenu;
} }
@ -877,7 +901,7 @@ public class GroupPane extends BorderPane {
if (t.getClickCount() == 1) { if (t.getClickCount() == 1) {
selectAllFiles(); selectAllFiles();
} }
if (selectionModel.getSelected().isEmpty() == false) { if (isNotEmpty(selectionModel.getSelected())) {
if (contextMenu == null) { if (contextMenu == null) {
contextMenu = buildContextMenu(); contextMenu = buildContextMenu();
} }

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2013-15 Basis Technology Corp. * Copyright 2013-18 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");
@ -55,9 +55,9 @@ import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent;
import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.imagegallery.FXMLConstructor; import org.sleuthkit.autopsy.imagegallery.FXMLConstructor;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
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.DrawableAttribute;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile;
@ -73,7 +73,7 @@ import org.sleuthkit.datamodel.TagName;
"MetaDataPane.valueColumn.headingName=Value"}) "MetaDataPane.valueColumn.headingName=Value"})
public class MetaDataPane extends DrawableUIBase { public class MetaDataPane extends DrawableUIBase {
private static final Logger LOGGER = Logger.getLogger(MetaDataPane.class.getName()); private static final Logger logger = Logger.getLogger(MetaDataPane.class.getName());
private static final KeyCodeCombination COPY_KEY_COMBINATION = new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_DOWN); private static final KeyCodeCombination COPY_KEY_COMBINATION = new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_DOWN);
@ -202,7 +202,7 @@ public class MetaDataPane extends DrawableUIBase {
@Override @Override
Task<Image> newReadImageTask(DrawableFile file) { Task<Image> newReadImageTask(DrawableFile file) {
return file.getThumbnailTask(); return getController().getThumbsCache().getThumbnailTask(file);
} }
public void updateAttributesTable() { public void updateAttributesTable() {

View File

@ -173,7 +173,7 @@ class GroupCellFactory {
private final InvalidationListener groupListener = new GroupListener<>(this); private final InvalidationListener groupListener = new GroupListener<>(this);
/** /**
* reference to group files listener that allows us to remove it from a * Reference to group files listener that allows us to remove it from a
* group when a new group is assigned to this Cell * group when a new group is assigned to this Cell
*/ */
@Override @Override

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2016 Basis Technology Corp. * Copyright 2016-18 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");
@ -38,6 +38,7 @@ import org.sleuthkit.autopsy.imagegallery.FXMLConstructor;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute; 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.GroupViewState;
/** /**
* Shows path based groups as a tree and others kinds of groups as a flat list ( * Shows path based groups as a tree and others kinds of groups as a flat list (
@ -77,17 +78,22 @@ final public class GroupTree extends NavPanel<TreeItem<GroupTreeNode>> {
groupTree.setShowRoot(false); groupTree.setShowRoot(false);
getGroupManager().getAnalyzedGroups().addListener((ListChangeListener.Change<? extends DrawableGroup> change) -> { getGroupManager().getAnalyzedGroups().addListener((ListChangeListener.Change<? extends DrawableGroup> change) -> {
GroupViewState oldState = getController().getViewState();
while (change.next()) { while (change.next()) {
change.getAddedSubList().stream().forEach(this::insertGroup); change.getAddedSubList().stream().forEach(this::insertGroup);
change.getRemoved().stream().forEach(this::removeFromTree); change.getRemoved().stream().forEach(this::removeFromTree);
} }
sortGroups(); Platform.runLater(() -> {
GroupTree.this.sortGroups(false);
Optional.ofNullable(oldState)
.flatMap(GroupViewState::getGroup)
.ifPresent(this::setFocusedGroup);
});
}); });
for (DrawableGroup g : getGroupManager().getAnalyzedGroups()) { getGroupManager().getAnalyzedGroups().forEach(this::insertGroup);
insertGroup(g); Platform.runLater(this::sortGroups);
}
sortGroups();
} }
/** /**
@ -102,12 +108,10 @@ final public class GroupTree extends NavPanel<TreeItem<GroupTreeNode>> {
if (treeItemForGroup != null) { if (treeItemForGroup != null) {
groupTree.getSelectionModel().select(treeItemForGroup); groupTree.getSelectionModel().select(treeItemForGroup);
Platform.runLater(() -> {
int row = groupTree.getRow(treeItemForGroup); int row = groupTree.getRow(treeItemForGroup);
if (row != -1) { if (row != -1) {
groupTree.scrollTo(row - 2); //put newly selected row 3 from the top groupTree.scrollTo(row - 2); //put newly selected row 3 from the top
} }
});
} }
} }

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2013-16 Basis Technology Corp. * Copyright 2013-18 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");
@ -18,7 +18,6 @@
*/ */
package org.sleuthkit.autopsy.imagegallery.gui.navpanel; package org.sleuthkit.autopsy.imagegallery.gui.navpanel;
import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -35,8 +34,8 @@ import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup;
/** /**
* A node in the nav/hash tree. Manages inserts and removals. Has parents and * A node in the nav/hash tree. Manages inserts and removals. Has parents and
* children. Does not have graphical properties these are configured in * children. Does not have graphical properties these are configured in
* {@link GroupTreeCell}. Each GroupTreeItem has a TreeNode which has a path * GroupTreeCell. Each GroupTreeItem has a TreeNode which has a path segment and
* segment and may or may not have a group * may or may not have a group
*/ */
class GroupTreeItem extends TreeItem<GroupTreeNode> { class GroupTreeItem extends TreeItem<GroupTreeNode> {
@ -131,7 +130,6 @@ class GroupTreeItem extends TreeItem<GroupTreeNode> {
} }
synchronized GroupTreeItem getTreeItemForPath(List<String> path) { synchronized GroupTreeItem getTreeItemForPath(List<String> path) {
if (path.isEmpty()) { if (path.isEmpty()) {
// end of recursion // end of recursion
return this; return this;
@ -154,9 +152,7 @@ class GroupTreeItem extends TreeItem<GroupTreeNode> {
if (parent != null) { if (parent != null) {
parent.childMap.remove(getValue().getPath()); parent.childMap.remove(getValue().getPath());
Platform.runLater(() -> { Platform.runLater(() -> parent.getChildren().remove(this));
parent.getChildren().removeAll(Collections.singleton(GroupTreeItem.this));
});
if (parent.childMap.isEmpty()) { if (parent.childMap.isEmpty()) {
parent.removeFromParent(); parent.removeFromParent();
@ -173,8 +169,6 @@ class GroupTreeItem extends TreeItem<GroupTreeNode> {
synchronized void resortChildren(Comparator<DrawableGroup> newComp) { synchronized void resortChildren(Comparator<DrawableGroup> newComp) {
this.comp = newComp; this.comp = newComp;
getChildren().sort(Comparator.comparing(treeItem -> treeItem.getValue().getGroup(), Comparator.nullsLast(comp))); getChildren().sort(Comparator.comparing(treeItem -> treeItem.getValue().getGroup(), Comparator.nullsLast(comp)));
for (GroupTreeItem ti : childMap.values()) { childMap.values().forEach(treeItem -> treeItem.resortChildren(comp));
ti.resortChildren(comp);
}
} }
} }

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2016 Basis Technology Corp. * Copyright 2016-18 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");
@ -22,6 +22,7 @@ import com.google.common.eventbus.Subscribe;
import java.util.Comparator; import java.util.Comparator;
import java.util.Optional; import java.util.Optional;
import java.util.function.Function; import java.util.function.Function;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.SelectionModel; import javafx.scene.control.SelectionModel;
@ -74,9 +75,9 @@ abstract class NavPanel<X> extends Tab {
sortChooser = new SortChooser<>(GroupComparators.getValues()); sortChooser = new SortChooser<>(GroupComparators.getValues());
sortChooser.setComparator(getDefaultComparator()); sortChooser.setComparator(getDefaultComparator());
sortChooser.sortOrderProperty().addListener(order -> sortGroups()); sortChooser.sortOrderProperty().addListener(order -> NavPanel.this.sortGroups());
sortChooser.comparatorProperty().addListener((observable, oldComparator, newComparator) -> { sortChooser.comparatorProperty().addListener((observable, oldComparator, newComparator) -> {
sortGroups(); NavPanel.this.sortGroups();
//only need to listen to changes in category if we are sorting by/ showing the uncategorized count //only need to listen to changes in category if we are sorting by/ showing the uncategorized count
if (newComparator == GroupComparators.UNCATEGORIZED_COUNT) { if (newComparator == GroupComparators.UNCATEGORIZED_COUNT) {
categoryManager.registerListener(NavPanel.this); categoryManager.registerListener(NavPanel.this);
@ -90,13 +91,20 @@ abstract class NavPanel<X> extends Tab {
toolBar.getItems().add(sortChooser); toolBar.getItems().add(sortChooser);
//keep selection in sync with controller //keep selection in sync with controller
controller.viewState().addListener(observable -> { controller.viewStateProperty().addListener(observable -> {
Optional.ofNullable(controller.viewState().get()) Platform.runLater(()
.map(GroupViewState::getGroup) -> Optional.ofNullable(controller.getViewState())
.ifPresent(this::setFocusedGroup); .flatMap(GroupViewState::getGroup)
.ifPresent(this::setFocusedGroup));
}); });
getSelectionModel().selectedItemProperty().addListener(o -> updateControllersGroup()); // notify controller about group selection in this view
getSelectionModel().selectedItemProperty()
.addListener((observable, oldItem, newSelectedItem) -> {
Optional.ofNullable(newSelectedItem)
.map(getDataItemMapper())
.ifPresent(group -> controller.advance(GroupViewState.tile(group)));
});
} }
/** /**
@ -106,7 +114,7 @@ abstract class NavPanel<X> extends Tab {
@Subscribe @Subscribe
public void handleCategoryChange(CategoryManager.CategoryChangeEvent event) { public void handleCategoryChange(CategoryManager.CategoryChangeEvent event) {
sortGroups(); Platform.runLater(this::sortGroups);
} }
/** /**
@ -121,28 +129,25 @@ abstract class NavPanel<X> extends Tab {
: comparator.reversed(); : 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));
}
/** /**
* Sort the groups in this view according to the currently selected sorting * Sort the groups in this view according to the currently selected sorting
* options. Attempts to maintain selection. * options. Attempts to maintain selection.
*/ */
@ThreadConfined(type = ThreadConfined.ThreadType.JFX) @ThreadConfined(type = ThreadConfined.ThreadType.JFX)
void sortGroups() { void sortGroups() {
sortGroups(true);
}
public void sortGroups(boolean preserveSelection) {
X selectedItem = getSelectionModel().getSelectedItem(); X selectedItem = getSelectionModel().getSelectedItem();
applyGroupComparator(); applyGroupComparator();
if (preserveSelection) {
Optional.ofNullable(selectedItem) Optional.ofNullable(selectedItem)
.map(getDataItemMapper()) .map(getDataItemMapper())
.ifPresent(this::setFocusedGroup); .ifPresent(this::setFocusedGroup);
} }
}
/** /**
* @return a function that maps the "native" data type of this view to a * @return a function that maps the "native" data type of this view to a

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -18,13 +18,20 @@
*/ */
package org.sleuthkit.autopsy.imagegallery.utils; package org.sleuthkit.autopsy.imagegallery.utils;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import javafx.concurrent.Task; import javafx.concurrent.Task;
/** /**
* *
*/ */
public class TaskUtils { public final class TaskUtils {
private TaskUtils() {
}
public static <T> Task<T> taskFrom(Callable<T> callable) { public static <T> Task<T> taskFrom(Callable<T> callable) {
return new Task<T>() { return new Task<T>() {
@ -35,6 +42,8 @@ public class TaskUtils {
}; };
} }
private TaskUtils() { public static ListeningExecutorService getExecutorForClass(Class<?> clazz) {
return MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(
new ThreadFactoryBuilder().setNameFormat("Image Gallery " + clazz.getSimpleName() + " BG Thread").build()));
} }
} }

View File

@ -1,5 +1,5 @@
#Updated by build script #Updated by build script
#Mon, 25 Jun 2018 17:19:36 -0400 #Mon, 03 Sep 2018 17:29:44 +0200
LBL_splash_window_title=Starting Autopsy LBL_splash_window_title=Starting Autopsy
SPLASH_HEIGHT=314 SPLASH_HEIGHT=314
SPLASH_WIDTH=538 SPLASH_WIDTH=538

View File

@ -1,4 +1,4 @@
#Updated by build script #Updated by build script
#Mon, 25 Jun 2018 17:19:36 -0400 #Mon, 03 Sep 2018 17:29:44 +0200
CTL_MainWindow_Title=Autopsy 4.8.0 CTL_MainWindow_Title=Autopsy 4.8.0
CTL_MainWindow_Title_No_Project=Autopsy 4.8.0 CTL_MainWindow_Title_No_Project=Autopsy 4.8.0