diff --git a/Core/nbproject/project.xml b/Core/nbproject/project.xml
index d5d64228b6..50ffa2b436 100644
--- a/Core/nbproject/project.xml
+++ b/Core/nbproject/project.xml
@@ -329,6 +329,7 @@
org.sleuthkit.autopsy.guiutils
org.sleuthkit.autopsy.healthmonitor
org.sleuthkit.autopsy.ingest
+ org.sleuthkit.autopsy.ingest.events
org.sleuthkit.autopsy.keywordsearchservice
org.sleuthkit.autopsy.menuactions
org.sleuthkit.autopsy.modules.encryptiondetection
diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java b/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java
index 0f7fb6b576..5a3b75ca69 100644
--- a/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java
+++ b/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java
@@ -107,7 +107,7 @@ public class ImageUtils {
* NOTE: Must be cleared when the case is changed.
*/
@Messages({"ImageUtils.ffmpegLoadedError.title=OpenCV FFMpeg",
- "ImageUtils.ffmpegLoadedError.msg=OpenCV FFMpeg library failed to load, see log for more details"})
+ "ImageUtils.ffmpegLoadedError.msg=OpenCV FFMpeg library failed to load, see log for more details"})
private static final ConcurrentHashMap cacheFileMap = new ConcurrentHashMap<>();
static {
@@ -218,7 +218,7 @@ public class ImageUtils {
}
return VideoUtils.isVideoThumbnailSupported(file)
- || isImageThumbnailSupported(file);
+ || isImageThumbnailSupported(file);
}
/**
@@ -413,7 +413,7 @@ public class ImageUtils {
String cacheDirectory = Case.getCurrentCaseThrows().getCacheDirectory();
return Paths.get(cacheDirectory, "thumbnails", fileID + ".png").toFile(); //NON-NLS
} 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;
}
});
diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/SelectionContext.java b/Core/src/org/sleuthkit/autopsy/directorytree/SelectionContext.java
index c204be3b00..de84a49e85 100644
--- a/Core/src/org/sleuthkit/autopsy/directorytree/SelectionContext.java
+++ b/Core/src/org/sleuthkit/autopsy/directorytree/SelectionContext.java
@@ -23,11 +23,13 @@ import org.openide.util.NbBundle;
import static org.sleuthkit.autopsy.directorytree.Bundle.*;
@NbBundle.Messages({"SelectionContext.dataSources=Data Sources",
+ "SelectionContext.dataSourceFiles=Data Source Files",
"SelectionContext.views=Views"})
enum SelectionContext {
DATA_SOURCES(SelectionContext_dataSources()),
VIEWS(SelectionContext_views()),
- OTHER(""); // Subnode of another node.
+ OTHER(""), // Subnode of another node.
+ DATA_SOURCE_FILES(SelectionContext_dataSourceFiles());
private final String displayName;
@@ -36,7 +38,7 @@ enum SelectionContext {
}
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;
} else if (name.equals(VIEWS.getName())) {
return VIEWS;
@@ -64,6 +66,16 @@ enum SelectionContext {
// One level below root node. Should be one of DataSources, Views, or Results
return SelectionContext.getContextFromName(n.getDisplayName());
} 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());
}
}
diff --git a/Core/src/org/sleuthkit/autopsy/ingest/events/DataSourceAnalysisCompletedEvent.java b/Core/src/org/sleuthkit/autopsy/ingest/events/DataSourceAnalysisCompletedEvent.java
index 0df66fabb6..4626b6858a 100644
--- a/Core/src/org/sleuthkit/autopsy/ingest/events/DataSourceAnalysisCompletedEvent.java
+++ b/Core/src/org/sleuthkit/autopsy/ingest/events/DataSourceAnalysisCompletedEvent.java
@@ -26,7 +26,7 @@ import org.sleuthkit.datamodel.Content;
* Event published when analysis (ingest) of a data source included in an ingest
* 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.
diff --git a/Core/src/org/sleuthkit/autopsy/ingest/events/DataSourceAnalysisStartedEvent.java b/Core/src/org/sleuthkit/autopsy/ingest/events/DataSourceAnalysisStartedEvent.java
index 6975120eae..b64a224da0 100644
--- a/Core/src/org/sleuthkit/autopsy/ingest/events/DataSourceAnalysisStartedEvent.java
+++ b/Core/src/org/sleuthkit/autopsy/ingest/events/DataSourceAnalysisStartedEvent.java
@@ -26,7 +26,7 @@ import org.sleuthkit.datamodel.Content;
* Event published when analysis (ingest) of a data source included in an ingest
* job is started.
*/
-public class DataSourceAnalysisStartedEvent extends DataSourceAnalysisEvent implements Serializable {
+public final class DataSourceAnalysisStartedEvent extends DataSourceAnalysisEvent implements Serializable {
private static final long serialVersionUID = 1L;
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/PromptDialogManager.java b/Core/src/org/sleuthkit/autopsy/timeline/PromptDialogManager.java
index cc58dfd80a..f9f1e12cfa 100644
--- a/Core/src/org/sleuthkit/autopsy/timeline/PromptDialogManager.java
+++ b/Core/src/org/sleuthkit/autopsy/timeline/PromptDialogManager.java
@@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
- * Copyright 2015-17 Basis Technology Corp.
+ * Copyright 2015-18 Basis Technology Corp.
* Contact: carrier sleuthkit org
*
* 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")
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;
static {
@@ -222,7 +220,7 @@ public final class PromptDialogManager {
dialog.setHeaderText(Bundle.PromptDialogManager_showTooManyFiles_headerText());
dialog.showAndWait();
}
-
+
@NbBundle.Messages({
"PromptDialogManager.showTimeLineDisabledMessage.contentText="
+ "Timeline functionality is not available yet."
diff --git a/CoreLibs/nbproject/project.xml b/CoreLibs/nbproject/project.xml
index b2aa57c54e..38da548a38 100644
--- a/CoreLibs/nbproject/project.xml
+++ b/CoreLibs/nbproject/project.xml
@@ -230,6 +230,7 @@
org.apache.commons.codec.digest
org.apache.commons.codec.language
org.apache.commons.codec.net
+ org.apache.commons.collections4
org.apache.commons.csv
org.apache.commons.io
org.apache.commons.io.comparator
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java
index cca4ec0f93..c983e6a721 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java
@@ -21,21 +21,24 @@ package org.sleuthkit.autopsy.imagegallery;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
-import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
+import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import javafx.application.Platform;
import javafx.beans.Observable;
+import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.ReadOnlyDoubleProperty;
-import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;
+import javafx.beans.property.ReadOnlyLongWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleBooleanProperty;
@@ -43,25 +46,14 @@ import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.concurrent.Worker;
-import javafx.geometry.Insets;
-import javafx.scene.Node;
-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 javax.annotation.Nonnull;
+import static org.apache.commons.collections4.CollectionUtils.isNotEmpty;
import org.netbeans.api.progress.ProgressHandle;
import org.openide.util.Cancellable;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.casemodule.Case.CaseType;
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.Logger;
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.datamodel.CategoryManager;
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.DrawableTagsManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.HashSetManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupManager;
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.modules.filetypeid.FileTypeDetector;
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.CaseDbTransaction;
import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.datamodel.TskData;
@@ -90,8 +82,7 @@ import org.sleuthkit.datamodel.TskData;
*/
public final class ImageGalleryController {
- private static final Logger LOGGER = Logger.getLogger(ImageGalleryController.class.getName());
- private static ImageGalleryController instance;
+ private static final Logger logger = Logger.getLogger(ImageGalleryController.class.getName());
/**
* 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 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 ReadOnlyIntegerWrapper dbTaskQueueSize = new ReadOnlyIntegerWrapper(0);
@@ -111,36 +102,23 @@ public final class ImageGalleryController {
private final History historyManager = new History<>();
private final UndoRedoManager undoManager = new UndoRedoManager();
- private final GroupManager groupManager = new GroupManager(this);
- private final HashSetManager hashSetManager = new HashSetManager();
- private final CategoryManager categoryManager = new CategoryManager(this);
- private final DrawableTagsManager tagsManager = new DrawableTagsManager(null);
-
- 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 final ThumbnailCache thumbnailCache = new ThumbnailCache(this);
+ private final GroupManager groupManager;
+ private final HashSetManager hashSetManager;
+ private final CategoryManager categoryManager;
+ private final DrawableTagsManager tagsManager;
private ListeningExecutorService dbExecutor;
- private SleuthkitCase sleuthKitCase;
- private DrawableDB db;
+ private final Case autopsyCase;
+ private final SleuthkitCase sleuthKitCase;
+ private final DrawableDB drawableDB;
- public static synchronized ImageGalleryController getDefault() {
- if (instance == null) {
- instance = new ImageGalleryController();
- }
- return instance;
+ public Case getAutopsyCase() {
+ return autopsyCase;
}
- public ReadOnlyBooleanProperty getMetaDataCollapsed() {
+ public ReadOnlyBooleanProperty metaDataCollapsedProperty() {
return metaDataCollapsed.getReadOnlyProperty();
}
@@ -148,19 +126,19 @@ public final class ImageGalleryController {
this.metaDataCollapsed.set(metaDataCollapsed);
}
- public ReadOnlyDoubleProperty thumbnailSizeProperty() {
- return thumbnailSize.getReadOnlyProperty();
+ public DoubleProperty thumbnailSizeProperty() {
+ return thumbnailSizeProp;
}
- private GroupViewState getViewState() {
+ public GroupViewState getViewState() {
return historyManager.getCurrentState();
}
- public ReadOnlyBooleanProperty regroupDisabled() {
+ public ReadOnlyBooleanProperty regroupDisabledProperty() {
return regroupDisabled.getReadOnlyProperty();
}
- public ReadOnlyObjectProperty viewState() {
+ public ReadOnlyObjectProperty viewStateProperty() {
return historyManager.currentState();
}
@@ -172,8 +150,8 @@ public final class ImageGalleryController {
return groupManager;
}
- synchronized public DrawableDB getDatabase() {
- return db;
+ public DrawableDB getDatabase() {
+ return drawableDB;
}
public void setListeningEnabled(boolean enabled) {
@@ -193,14 +171,9 @@ public final class ImageGalleryController {
Platform.runLater(() -> {
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();
}
@@ -209,50 +182,57 @@ public final class ImageGalleryController {
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 {
- //if we just turned on listening and a case is open and that case is not up to date
- if (newValue && !oldValue && ImageGalleryModule.isDrawableDBStale(Case.getCurrentCaseThrows())) {
+ // if we just turned on listening and a single-user case is open and that case is not up to date, then rebuild it
+ // 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
- queueDBTask(new CopyAnalyzedFiles(instance, db, sleuthKitCase));
+ this.rebuildDB();
}
+
} 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) -> {
- //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) -> {
+ viewStateProperty().addListener((Observable observable) -> {
//when the viewed group changes, clear the selection and the undo/redo history
selectionModel.clearSelection();
undoManager.clear();
});
- regroupDisabled.addListener(observable -> checkForGroups());
-
IngestManager ingestManager = IngestManager.getInstance();
- PropertyChangeListener ingestEventHandler =
- propertyChangeEvent -> Platform.runLater(this::updateRegroupDisabled);
+ PropertyChangeListener ingestEventHandler
+ = propertyChangeEvent -> Platform.runLater(this::updateRegroupDisabled);
ingestManager.addIngestModuleEventListener(ingestEventHandler);
ingestManager.addIngestJobEventListener(ingestEventHandler);
dbTaskQueueSize.addListener(obs -> this.updateRegroupDisabled());
+
}
public ReadOnlyBooleanProperty getCanAdvance() {
@@ -264,10 +244,7 @@ public final class ImageGalleryController {
}
@ThreadConfined(type = ThreadConfined.ThreadType.ANY)
- public void advance(GroupViewState newState, boolean forceShowTree) {
- if (forceShowTree && showTree != null) {
- showTree.run();
- }
+ public void advance(GroupViewState newState) {
historyManager.advance(newState);
}
@@ -284,131 +261,92 @@ public final class ImageGalleryController {
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.
*
* @param theNewCase the case to configure the controller for
+ *
+ * @throws org.sleuthkit.datamodel.TskCoreException
*/
- public synchronized void setCase(Case theNewCase) {
- if (null == theNewCase) {
- reset();
- } else {
- this.sleuthKitCase = theNewCase.getSleuthkitCase();
- this.db = DrawableDB.getDrawableDB(ImageGalleryModule.getModuleOutputDir(theNewCase), 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();
- }
+ /**
+ * Rebuilds the DrawableDB database.
+ *
+ */
+ public void rebuildDB() {
+ // queue a rebuild task for each stale data source
+ getStaleDataSourceIds().forEach(dataSourceObjId -> queueDBTask(new CopyAnalyzedFiles(dataSourceObjId, this)));
}
/**
* reset the state of the controller (eg if the case is closed)
*/
public synchronized void reset() {
- LOGGER.info("resetting ImageGalleryControler to initial state."); //NON-NLS
+ logger.info("Closing ImageGalleryControler for case."); //NON-NLS
+
selectionModel.clearSelection();
- setListeningEnabled(false);
- ThumbnailCache.getDefault().clearCache();
+ thumbnailCache.clearCache();
historyManager.clear();
- groupManager.clear();
- tagsManager.clearFollowUpTagName();
- tagsManager.unregisterListener(groupManager);
- tagsManager.unregisterListener(categoryManager);
+ groupManager.reset();
+
shutDownDBExecutor();
+ dbExecutor = getNewDBExecutor();
+ }
- if (toolbar != null) {
- toolbar.reset();
+ /**
+ * Checks if the datasources table in drawable DB is stale.
+ *
+ * @return true if datasources table is stale
+ */
+ public boolean isDataSourcesTableStale() {
+ return isNotEmpty(getStaleDataSourceIds());
+ }
+
+ /**
+ * 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 getStaleDataSourceIds() {
+
+ Set staleDataSourceIds = new HashSet<>();
+
+ // no current case open to check
+ if ((null == getDatabase()) || (null == getSleuthKitCase())) {
+ return staleDataSourceIds;
}
- if (db != null) {
- db.closeDBCon();
+ try {
+ Map knownDataSourceIds = getDatabase().getDataSourceDbBuildStatus();
+
+ List dataSources = getSleuthKitCase().getDataSources();
+ Set 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 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;
}
- db = null;
+
}
synchronized private void shutDownDBExecutor() {
@@ -417,7 +355,7 @@ public final class ImageGalleryController {
try {
dbExecutor.awaitTermination(30, TimeUnit.SECONDS);
} 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));
}
- @Nullable
- synchronized public DrawableFile getFileFromId(Long fileID) throws TskCoreException {
- 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 DrawableFile getFileFromID(Long fileID) throws TskCoreException {
+ return drawableDB.getFileFromID(fileID);
}
public ReadOnlyDoubleProperty 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() {
return hashSetManager;
}
@@ -501,10 +407,6 @@ public final class ImageGalleryController {
return tagsManager;
}
- public void setShowTree(Runnable showTree) {
- this.showTree = showTree;
- }
-
public UndoRedoManager getUndoManager() {
return undoManager;
}
@@ -515,6 +417,12 @@ public final class ImageGalleryController {
public synchronized SleuthkitCase getSleuthKitCase() {
return sleuthKitCase;
+
+ }
+
+ public ThumbnailCache getThumbsCache() {
+ return thumbnailCache;
+
}
/**
@@ -594,7 +502,7 @@ public final class ImageGalleryController {
return file;
}
- public FileTask(AbstractFile f, DrawableDB taskDB) {
+ FileTask(AbstractFile f, DrawableDB taskDB) {
super();
this.file = f;
this.taskDB = taskDB;
@@ -604,7 +512,7 @@ public final class ImageGalleryController {
/**
* 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) {
super(f, taskDB);
@@ -631,7 +539,7 @@ public final class ImageGalleryController {
/**
* 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) {
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",
"BulkTask.stopCopy.status=Stopping copy to drawable db task.",
"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 private class BulkTransferTask extends BackgroundTask {
+ abstract static class BulkTransferTask extends BackgroundTask {
- static private final String FILE_EXTENSION_CLAUSE =
- "(name LIKE '%." //NON-NLS
- + String.join("' OR name LIKE '%.", FileTypeUtils.getAllSupportedExtensions()) //NON-NLS
- + "')";
+ static private final String FILE_EXTENSION_CLAUSE
+ = "(extension LIKE '" //NON-NLS
+ + String.join("' OR extension LIKE '", FileTypeUtils.getAllSupportedExtensions()) //NON-NLS
+ + "') ";
- static private final String MIMETYPE_CLAUSE =
- "(mime_type LIKE '" //NON-NLS
- + String.join("' OR mime_type LIKE '", FileTypeUtils.getAllSupportedMimeTypes()) //NON-NLS
- + "') ";
+ static private final String MIMETYPE_CLAUSE
+ = "(mime_type LIKE '" //NON-NLS
+ + String.join("' OR mime_type LIKE '", FileTypeUtils.getAllSupportedMimeTypes()) //NON-NLS
+ + "') ";
- static final String DRAWABLE_QUERY =
- //grab files with supported extension
- "(" + FILE_EXTENSION_CLAUSE
- //grab files with supported mime-types
- + " OR " + MIMETYPE_CLAUSE //NON-NLS
- //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
+ private final String DRAWABLE_QUERY;
+ private final String DATASOURCE_CLAUSE;
- final ImageGalleryController controller;
- final DrawableDB taskDB;
- final SleuthkitCase tskCase;
+ protected final ImageGalleryController controller;
+ protected final DrawableDB taskDB;
+ protected final SleuthkitCase tskCase;
+ protected final long dataSourceObjId;
- ProgressHandle progressHandle;
+ private ProgressHandle progressHandle;
+ private boolean taskCompletionStatus;
- BulkTransferTask(ImageGalleryController controller, DrawableDB taskDB, SleuthkitCase tskCase) {
+ BulkTransferTask(long dataSourceObjId, ImageGalleryController controller) {
this.controller = controller;
- this.taskDB = taskDB;
- this.tskCase = tskCase;
+ 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
+ + " OR " + MIMETYPE_CLAUSE //NON-NLS
+ //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
}
/**
- *
+ * Do any cleanup for this task.
+ *
* @param success true if the transfer was successful
*/
abstract void cleanup(boolean success);
- abstract List 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 getFiles() throws TskCoreException {
+ return tskCase.findAllFilesWhere(DRAWABLE_QUERY);
+ }
@Override
public void run() {
@@ -706,24 +637,32 @@ public final class ImageGalleryController {
progressHandle.start();
updateMessage(Bundle.CopyAnalyzedFiles_populatingDb_status());
+ DrawableDB.DrawableTransaction drawableDbTransaction = null;
+ CaseDbTransaction caseDbTransaction = null;
try {
//grab all files with supported extension or detected mime types
final List files = getFiles();
progressHandle.switchToDeterminate(files.size());
+ taskDB.insertOrUpdateDataSource(dataSourceObjId, DrawableDB.DrawableDbBuildStatusEnum.IN_PROGRESS);
+
updateProgress(0.0);
+ taskCompletionStatus = true;
+ int workDone = 0;
//do in transaction
- DrawableDB.DrawableTransaction tr = taskDB.beginTransaction();
- int workDone = 0;
+ drawableDbTransaction = taskDB.beginTransaction();
+ caseDbTransaction = tskCase.beginTransaction();
for (final AbstractFile f : files) {
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();
+
break;
}
- processFile(f, tr);
+ processFile(f, drawableDbTransaction, caseDbTransaction);
workDone++;
progressHandle.progress(f.getName(), workDone);
@@ -737,23 +676,42 @@ public final class ImageGalleryController {
updateProgress(1.0);
progressHandle.start();
- taskDB.commitTransaction(tr, true);
+ caseDbTransaction.commit();
+ taskDB.commitTransaction(drawableDbTransaction, true);
} 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());
- 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());
cleanup(false);
return;
} finally {
progressHandle.finish();
+ if (taskCompletionStatus) {
+ taskDB.insertOrUpdateDataSource(dataSourceObjId, DrawableDB.DrawableDbBuildStatusEnum.COMPLETE);
+ }
updateMessage("");
updateProgress(-1.0);
}
- cleanup(true);
+ cleanup(taskCompletionStatus);
}
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",
"CopyAnalyzedFiles.stopCopy.status=Stopping copy to drawable db task.",
"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) {
- super(controller, taskDB, tskCase);
+ CopyAnalyzedFiles(long dataSourceObjId, ImageGalleryController controller) {
+ super(dataSourceObjId, controller);
}
@Override
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
- List getFiles() throws TskCoreException {
- return tskCase.findAllFilesWhere(DRAWABLE_QUERY);
- }
-
- @Override
- void processFile(AbstractFile f, DrawableDB.DrawableTransaction tr) {
+ void processFile(AbstractFile f, DrawableDB.DrawableTransaction tr, CaseDbTransaction caseDbTransaction) throws TskCoreException {
final boolean known = f.getKnown() == TskData.FileKnown.KNOWN;
if (known) {
@@ -791,13 +746,21 @@ public final class ImageGalleryController {
} else {
try {
- if (FileTypeUtils.hasDrawableMIMEType(f)) { //supported mimetype => analyzed
- taskDB.updateFile(DrawableFile.create(f, true, false), tr);
- } else { //unsupported mimtype => analyzed but shouldn't include
- taskDB.removeFile(f.getId(), tr);
+ //supported mimetype => analyzed
+ if (null != f.getMIMEType() && FileTypeUtils.hasDrawableMIMEType(f)) {
+ 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);
+ }
}
} 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"
* 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
- *
- * 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"})
- static private class PrePopulateDataSourceFiles extends BulkTransferTask {
-
- private static final Logger LOGGER = Logger.getLogger(PrePopulateDataSourceFiles.class.getName());
-
- private final Content dataSource;
+ static class PrePopulateDataSourceFiles extends BulkTransferTask {
/**
- *
- * @param dataSourceId Data source object ID
+ * @param dataSourceObjId The object ID of the DataSource that is being
+ * pre-populated into the DrawableDB.
+ * @param controller The controller for this task.
*/
- PrePopulateDataSourceFiles(Content dataSource, ImageGalleryController controller, DrawableDB taskDB, SleuthkitCase tskCase) {
- super(controller, taskDB, tskCase);
- this.dataSource = dataSource;
+ PrePopulateDataSourceFiles(long dataSourceObjId, ImageGalleryController controller) {
+ super(dataSourceObjId, controller);
}
@Override
@@ -838,14 +794,8 @@ public final class ImageGalleryController {
}
@Override
- void processFile(final AbstractFile f, DrawableDB.DrawableTransaction tr) {
- taskDB.insertFile(DrawableFile.create(f, false, false), tr);
- }
-
- @Override
- List getFiles() throws TskCoreException {
- long datasourceID = dataSource.getDataSource().getId();
- return tskCase.findAllFilesWhere("data_source_obj_id = " + datasourceID + " AND " + DRAWABLE_QUERY);
+ void processFile(final AbstractFile f, DrawableDB.DrawableTransaction tr, CaseDbTransaction caseDBTransaction) {
+ taskDB.insertFile(DrawableFile.create(f, false, false), tr, caseDBTransaction);
}
@Override
@@ -854,116 +804,4 @@ public final class ImageGalleryController {
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;
- }
- }
- }
}
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryModule.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryModule.java
index a5f13c1d9a..07baaf72a9 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryModule.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryModule.java
@@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
- * Copyright 2013-15 Basis Technology Corp.
+ * Copyright 2013-18 Basis Technology Corp.
* Contact: carrier sleuthkit org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,32 +18,77 @@
*/
package org.sleuthkit.autopsy.imagegallery;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
import java.nio.file.Path;
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 org.openide.util.NbBundle;
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.MessageNotifyUtil;
+import org.sleuthkit.autopsy.events.AutopsyEvent;
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.datamodel.AbstractFile;
+import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.TskCoreException;
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"})
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 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() {
return MODULE_NAME;
}
-
/**
* get the Path to the Case's ImageGallery ModuleOutput subfolder; ie
* ".../[CaseName]/ModuleOutput/Image Gallery/"
@@ -53,7 +98,7 @@ public class ImageGalleryModule {
*
* @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());
}
@@ -83,19 +128,10 @@ public class ImageGalleryModule {
* @return true if the drawable db is out of date for the given case, false
* otherwise
*/
- public static boolean isDrawableDBStale(Case c) {
- if (c != null) {
- String stale = new PerCaseProperties(c).getConfigSetting(ImageGalleryModule.MODULE_NAME, PerCaseProperties.STALE);
- return StringUtils.isNotBlank(stale) ? Boolean.valueOf(stale) : true;
- } else {
- return false;
- }
+ public static boolean isDrawableDBStale(Case c) throws TskCoreException {
+ return new ImageGalleryController(c).isDataSourcesTableStale();
}
-
-
-
-
/**
* Is the given file 'supported' and not 'known'(nsrl hash hit). If so we
* should include it in {@link DrawableDB} and UI
@@ -105,7 +141,187 @@ public class ImageGalleryModule {
* @return true if the given {@link AbstractFile} is "drawable" and not
* '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);
}
+
+ /**
+ * 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
+ }
+ });
+ }
+ }
+ }
}
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryOptionsPanel.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryOptionsPanel.java
index 7fbcaac1f0..80c35be23b 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryOptionsPanel.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryOptionsPanel.java
@@ -18,13 +18,10 @@
*/
package org.sleuthkit.autopsy.imagegallery;
-import java.awt.event.ActionEvent;
-import java.util.logging.Level;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.autopsy.ingest.IngestManager;
-import org.sleuthkit.autopsy.coreutils.Logger;
/**
* 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);
});
- enabledByDefaultBox.addActionListener((ActionEvent e) -> {
- controller.changed();
- });
-
- enabledForCaseBox.addActionListener((ActionEvent e) -> {
- controller.changed();
- });
+ enabledByDefaultBox.addActionListener(actionEvent -> controller.changed());
+ enabledForCaseBox.addActionListener(actionEvent -> controller.changed());
}
/**
@@ -204,19 +196,18 @@ final class ImageGalleryOptionsPanel extends javax.swing.JPanel {
void store() {
ImageGalleryPreferences.setEnabledByDefault(enabledByDefaultBox.isSelected());
- ImageGalleryController.getDefault().setListeningEnabled(enabledForCaseBox.isSelected());
+
ImageGalleryPreferences.setGroupCategorizationWarningDisabled(groupCategorizationWarningBox.isSelected());
// If a case is open, save the per case setting
try {
Case openCase = Case.getCurrentCaseThrows();
+ ImageGalleryModule.getController().setListeningEnabled(enabledForCaseBox.isSelected());
new PerCaseProperties(openCase).setConfigSetting(ImageGalleryModule.getModuleName(), PerCaseProperties.ENABLED, Boolean.toString(enabledForCaseBox.isSelected()));
} catch (NoCurrentCaseException ex) {
// It's not an error if there's no case open
}
-
-
-
+
}
/**
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryPreferences.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryPreferences.java
index 93329c3e62..37be779969 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryPreferences.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryPreferences.java
@@ -46,8 +46,7 @@ public class ImageGalleryPreferences {
* @return true if new cases should have image analyzer enabled.
*/
public static boolean isEnabledByDefault() {
- final boolean aBoolean = preferences.getBoolean(ENABLED_BY_DEFAULT, true);
- return aBoolean;
+ return preferences.getBoolean(ENABLED_BY_DEFAULT, true);
}
public static void setEnabledByDefault(boolean b) {
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryTopComponent.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryTopComponent.java
index a6ce70c3ac..9263913748 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryTopComponent.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryTopComponent.java
@@ -18,27 +18,52 @@
*/
package org.sleuthkit.autopsy.imagegallery;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Optional;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javafx.application.Platform;
+import javafx.beans.Observable;
import javafx.embed.swing.JFXPanel;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
import javafx.scene.Scene;
+import javafx.scene.control.ChoiceDialog;
+import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.SplitPane;
import javafx.scene.control.TabPane;
+import javafx.scene.layout.Background;
+import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
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.ExplorerUtils;
import org.openide.util.Lookup;
+import org.openide.util.NbBundle;
import org.openide.util.NbBundle.Messages;
import org.openide.windows.Mode;
import org.openide.windows.RetainLocation;
import org.openide.windows.TopComponent;
import org.openide.windows.WindowManager;
+import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
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.SummaryTablePane;
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.navpanel.GroupTree;
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.
@@ -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 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 final ExplorerManager em = new ExplorerManager();
private final Lookup lookup = (ExplorerUtils.createLookup(em, getActionMap()));
- private final ImageGalleryController controller = ImageGalleryController.getDefault();
+ private ImageGalleryController controller;
private SplitPane splitPane;
private StackPane centralStack;
- private BorderPane borderPane = new BorderPane();
+ private final BorderPane borderPane = new BorderPane();
private StackPane fullUIStack;
private MetaDataPane metaDataTable;
private GroupPane groupPane;
@@ -88,24 +116,97 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl
private VBox leftPane;
private Scene myScene;
- public static void openTopComponent() {
- //TODO:eventually move to this model, throwing away everything and rebuilding controller groupmanager etc for each case.
- // synchronized (OpenTimelineAction.class) {
- // if (timeLineController == null) {
- // timeLineController = new TimeLineController();
- // LOGGER.log(Level.WARNING, "Failed to get TimeLineController from lookup. Instantiating one directly.S");
- // }
- // }
- // timeLineController.openTimeLine();
- final TopComponent tc = WindowManager.getDefault().findTopComponent(PREFERRED_ID);
- if (tc != null) {
- topComponentInitialized = true;
- if (tc.isOpened() == false) {
- tc.open();
- }
- tc.toFront();
- tc.requestActive();
+ private Node infoOverlay;
+ private final Region infoOverLayBackground = new TranslucentRegion();
+
+ /**
+ * Returns whether the ImageGallery window is open or not.
+ *
+ * @return true, if Image gallery is opened, false otherwise
+ */
+ public static boolean isImageGalleryOpen() {
+
+ 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;
+ if (topComponent.isOpened()) {
+ showTopComponent(topComponent);
+ return;
+ }
+
+ List 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 dataSourceNames = new HashMap<>();
+ dataSourceNames.put("All", null);
+ dataSources.forEach(dataSource -> dataSourceNames.put(dataSource.getName(), dataSource));
+
+ Platform.runLater(() -> {
+ ChoiceDialog 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 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() {
@@ -115,48 +216,57 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl
try {
etc.close();
} 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());
initComponents();
+ setController(ImageGalleryModule.getController());
+ }
- Platform.runLater(() -> {//initialize jfx ui
- fullUIStack = new StackPane(); //this is passed into controller
- myScene = new Scene(fullUIStack);
- jfxPanel.setScene(myScene);
- groupPane = new GroupPane(controller);
- centralStack = new StackPane(groupPane); //this is passed into controller
- fullUIStack.getChildren().add(borderPane);
- splitPane = new SplitPane();
- borderPane.setCenter(splitPane);
- Toolbar toolbar = new Toolbar(controller);
- borderPane.setTop(toolbar);
- borderPane.setBottom(new StatusBar(controller));
+ 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
+ myScene = new Scene(fullUIStack);
+ jfxPanel.setScene(myScene);
+ groupPane = new GroupPane(controller);
+ centralStack = new StackPane(groupPane); //this is passed into controller
+ fullUIStack.getChildren().add(borderPane);
+ splitPane = new SplitPane();
+ borderPane.setCenter(splitPane);
+ Toolbar toolbar = new Toolbar(controller);
+ borderPane.setTop(toolbar);
+ borderPane.setBottom(new StatusBar(controller));
+ metaDataTable = new MetaDataPane(controller);
+ groupTree = new GroupTree(controller);
+ hashHitList = new HashHitGroupList(controller);
+ TabPane tabPane = new TabPane(groupTree, hashHitList);
+ tabPane.setPrefWidth(TabPane.USE_COMPUTED_SIZE);
+ tabPane.setMinWidth(TabPane.USE_PREF_SIZE);
+ VBox.setVgrow(tabPane, Priority.ALWAYS);
+ leftPane = new VBox(tabPane, new SummaryTablePane(controller));
+ SplitPane.setResizableWithParent(leftPane, Boolean.FALSE);
+ SplitPane.setResizableWithParent(groupPane, Boolean.TRUE);
+ SplitPane.setResizableWithParent(metaDataTable, Boolean.FALSE);
+ splitPane.getItems().addAll(leftPane, centralStack, metaDataTable);
+ splitPane.setDividerPositions(0.1, 1.0);
- metaDataTable = new MetaDataPane(controller);
+ controller.regroupDisabledProperty().addListener((Observable observable) -> checkForGroups());
+ controller.getGroupManager().getAnalyzedGroups().addListener((Observable observable) -> Platform.runLater(() -> checkForGroups()));
- groupTree = new GroupTree(controller);
- hashHitList = new HashHitGroupList(controller);
-
- TabPane tabPane = new TabPane(groupTree, hashHitList);
- tabPane.setPrefWidth(TabPane.USE_COMPUTED_SIZE);
- tabPane.setMinWidth(TabPane.USE_PREF_SIZE);
- VBox.setVgrow(tabPane, Priority.ALWAYS);
- leftPane = new VBox(tabPane, new SummaryTablePane(controller));
- SplitPane.setResizableWithParent(leftPane, Boolean.FALSE);
- SplitPane.setResizableWithParent(groupPane, Boolean.TRUE);
- SplitPane.setResizableWithParent(metaDataTable, Boolean.FALSE);
- splitPane.getItems().addAll(leftPane, centralStack, metaDataTable);
- splitPane.setDividerPositions(0.1, 1.0);
-
- ImageGalleryController.getDefault().setStacks(fullUIStack, centralStack);
- ImageGalleryController.getDefault().setToolbar(toolbar);
- 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() {
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);
+ }
+ }
}
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/OnStart.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/OnStart.java
index b222b2bc7c..cf928514dd 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/OnStart.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/OnStart.java
@@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
- * Copyright 2013 Basis Technology Corp.
+ * Copyright 2013-2018 Basis Technology Corp.
* Contact: carrier sleuthkit org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,26 +18,19 @@
*/
package org.sleuthkit.autopsy.imagegallery;
-import org.sleuthkit.autopsy.coreutils.Logger;
-
/**
- *
- * The {@link org.openide.modules.OnStart} annotation tells NetBeans to invoke
- * this class's {@link OnStart#run()} method
+ * The org.openide.modules.OnStart annotation tells NetBeans to invoke this
+ * class's run method.
*/
@org.openide.modules.OnStart
public class OnStart implements Runnable {
- static private final Logger LOGGER = Logger.getLogger(OnStart.class.getName());
-
/**
- *
- *
- * This method is invoked by virtue of the {@link OnStart} annotation on the
- * {@link ImageGalleryModule} class
+ * This method is invoked by virtue of the OnStart annotation on the this
+ * class
*/
@Override
public void run() {
- ImageGalleryController.getDefault().onStart();
+ ImageGalleryModule.onStart();
}
}
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/PerCaseProperties.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/PerCaseProperties.java
index d87b13e880..56811352f0 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/PerCaseProperties.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/PerCaseProperties.java
@@ -40,8 +40,6 @@ class PerCaseProperties {
public static final String ENABLED = "enabled"; //NON-NLS
- public static final String STALE = "stale"; //NON-NLS
-
private final Case theCase;
PerCaseProperties(Case c) {
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ThumbnailCache.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ThumbnailCache.java
index 78b260e731..c6fdd893eb 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ThumbnailCache.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ThumbnailCache.java
@@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
- * Copyright 2013-15 Basis Technology Corp.
+ * Copyright 2013-18 Basis Technology Corp.
* Contact: carrier sleuthkit org
*
* 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
* 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;
@@ -71,10 +75,6 @@ public enum ThumbnailCache {
.softValues()
.expireAfterAccess(10, TimeUnit.MINUTES).build();
- public static ThumbnailCache getDefault() {
- return instance;
- }
-
/**
* currently desired icon size. is bound in {@link Toolbar}
*/
@@ -109,7 +109,7 @@ public enum ThumbnailCache {
@Nullable
public Image get(Long fileID) {
try {
- return get(ImageGalleryController.getDefault().getFileFromId(fileID));
+ return get(controller.getFileFromID(fileID));
} catch (TskCoreException ex) {
LOGGER.log(Level.WARNING, "Failed to load thumbnail for file: " + fileID, ex.getCause()); //NON-NLS
return null;
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddTagAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddTagAction.java
index cd40efdb55..dbb27e81dc 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddTagAction.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddTagAction.java
@@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
- * Copyright 2013-2017 Basis Technology Corp.
+ * Copyright 2013-2018 Basis Technology Corp.
* Contact: carrier sleuthkit org
*
* 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.ActionUtils;
import org.openide.util.NbBundle;
-import org.openide.util.NbBundle.Messages;
import org.openide.windows.TopComponent;
import org.openide.windows.WindowManager;
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.datamodel.ContentTag;
import org.sleuthkit.datamodel.TagName;
-import org.sleuthkit.datamodel.TskData;
import org.sleuthkit.datamodel.TskCoreException;
+import org.sleuthkit.datamodel.TskData;
/**
* Instances of this Action allow users to apply tags to content.
@@ -75,14 +74,14 @@ public class AddTagAction extends Action {
setEventHandler(actionEvent -> addTagWithComment(""));
}
- static public Menu getTagMenu(ImageGalleryController controller) {
+ static public Menu getTagMenu(ImageGalleryController controller) throws TskCoreException {
return new TagMenu(controller);
}
private void addTagWithComment(String comment) {
addTagsToFiles(tagName, comment, selectedFileIDs);
}
-
+
@NbBundle.Messages({"# {0} - fileID",
"AddDrawableTagAction.addTagsToFiles.alert=Unable to tag file {0}."})
private void addTagsToFiles(TagName tagName, String comment, Set selectedFiles) {
@@ -94,7 +93,7 @@ public class AddTagAction extends Action {
DrawableTagsManager tagsManager = controller.getTagsManager();
for (Long fileID : selectedFiles) {
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
List contentTags = tagsManager.getContentTags(file);
@@ -141,7 +140,7 @@ public class AddTagAction extends Action {
"AddDrawableTagAction.displayName.singular=Tag File"})
private static class TagMenu extends Menu {
- TagMenu(ImageGalleryController controller) {
+ TagMenu(ImageGalleryController controller) throws TskCoreException {
setGraphic(new ImageView(DrawableAttribute.TAGS.getIcon()));
ObservableSet selectedFileIDs = controller.getSelectionModel().getSelected();
setText(selectedFileIDs.size() > 1
@@ -163,11 +162,10 @@ public class AddTagAction extends Action {
empty.setDisable(true);
quickTagMenu.getItems().add(empty);
} else {
- for (final TagName tagName : tagNames) {
- AddTagAction addDrawableTagAction = new AddTagAction(controller, tagName, selectedFileIDs);
- MenuItem tagNameItem = ActionUtils.createMenuItem(addDrawableTagAction);
- quickTagMenu.getItems().add(tagNameItem);
- }
+ tagNames.stream()
+ .map(tagName -> new AddTagAction(controller, tagName, selectedFileIDs))
+ .map(ActionUtils::createMenuItem)
+ .forEachOrdered(quickTagMenu.getItems()::add);
}
/*
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeAction.java
index 14944dff09..c6d540dbb3 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeAction.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeAction.java
@@ -138,7 +138,7 @@ public class CategorizeAction extends Action {
TagName catZeroTagName = categoryManager.getTagName(DhsImageCategory.ZERO);
for (long fileID : fileIDs) {
try {
- DrawableFile file = controller.getFileFromId(fileID); //drawable db access
+ DrawableFile file = controller.getFileFromID(fileID); //drawable db access
if (createUndo) {
DhsImageCategory oldCat = file.getCategory(); //drawable db access
TagName oldCatTagName = categoryManager.getTagName(oldCat);
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeGroupAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeGroupAction.java
index f568fbd105..b49e1ca30c 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeGroupAction.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeGroupAction.java
@@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
- * Copyright 2015-16 Basis Technology Corp.
+ * Copyright 2015-18 Basis Technology Corp.
* Contact: carrier sleuthkit org
*
* 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.layout.Priority;
import javafx.scene.layout.VBox;
+import static org.apache.commons.lang.ObjectUtils.notEqual;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryPreferences;
-import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.datamodel.TskCoreException;
/**
@@ -52,32 +53,35 @@ public class CategorizeGroupAction extends CategorizeAction {
public CategorizeGroupAction(DhsImageCategory newCat, ImageGalleryController controller) {
super(controller, newCat, null);
setEventHandler(actionEvent -> {
- ObservableList fileIDs = controller.viewState().get().getGroup().getFileIDs();
+ controller.getViewState().getGroup().ifPresent(group -> {
+ ObservableList fileIDs = group.getFileIDs();
- if (ImageGalleryPreferences.isGroupCategorizationWarningDisabled()) {
- //if they have preveiously disabled the warning, just go ahead and apply categories.
- addCatToFiles(ImmutableSet.copyOf(fileIDs));
- } else {
- final Map catCountMap = new HashMap<>();
-
- for (Long fileID : fileIDs) {
- try {
- DhsImageCategory category = controller.getFileFromId(fileID).getCategory();
- if (false == DhsImageCategory.ZERO.equals(category) && newCat.equals(category) == false) {
- catCountMap.merge(category, 1L, Long::sum);
- }
- } catch (TskCoreException ex) {
- LOGGER.log(Level.SEVERE, "Failed to categorize files.", ex);
- }
- }
-
- if (catCountMap.isEmpty()) {
- //if there are not going to be any categories overwritten, skip the warning.
+ if (ImageGalleryPreferences.isGroupCategorizationWarningDisabled()) {
+ //if they have preveiously disabled the warning, just go ahead and apply categories.
addCatToFiles(ImmutableSet.copyOf(fileIDs));
} else {
- showConfirmationDialog(catCountMap, newCat, fileIDs);
+ final Map catCountMap = new HashMap<>();
+
+ for (Long fileID : fileIDs) {
+ try {
+ DhsImageCategory category = controller.getFileFromID(fileID).getCategory();
+ if (false == DhsImageCategory.ZERO.equals(category) && newCat.equals(category) == false) {
+ catCountMap.merge(category, 1L, Long::sum);
+ }
+ } catch (TskCoreException ex) {
+ LOGGER.log(Level.SEVERE, "Failed to categorize files.", ex);
+ }
+ }
+
+ if (catCountMap.isEmpty()) {
+ //if there are not going to be any categories overwritten, skip the warning.
+ addCatToFiles(ImmutableSet.copyOf(fileIDs));
+ } else {
+ showConfirmationDialog(catCountMap, newCat, fileIDs);
+ }
}
- }
+ });
+
});
}
@@ -88,19 +92,18 @@ public class CategorizeGroupAction extends CategorizeAction {
"CategorizeGroupAction.fileCountHeader=Files in the following categories will have their categories overwritten: "})
private void showConfirmationDialog(final Map catCountMap, DhsImageCategory newCat, ObservableList fileIDs) {
- ButtonType categorizeButtonType =
- new ButtonType(Bundle.CategorizeGroupAction_OverwriteButton_text(), ButtonBar.ButtonData.APPLY);
+ ButtonType categorizeButtonType
+ = new ButtonType(Bundle.CategorizeGroupAction_OverwriteButton_text(), ButtonBar.ButtonData.APPLY);
VBox textFlow = new VBox();
for (Map.Entry entry : catCountMap.entrySet()) {
- if (entry.getKey().equals(newCat) == false) {
- if (entry.getValue() > 0) {
- Label label = new Label(Bundle.CategorizeGroupAction_fileCountMessage(entry.getValue(), entry.getKey().getDisplayName()),
- entry.getKey().getGraphic());
- label.setContentDisplay(ContentDisplay.RIGHT);
- textFlow.getChildren().add(label);
- }
+ if (entry.getValue() > 0
+ && notEqual(entry.getKey(), newCat)) {
+ Label label = new Label(Bundle.CategorizeGroupAction_fileCountMessage(entry.getValue(), entry.getKey().getDisplayName()),
+ entry.getKey().getGraphic());
+ label.setContentDisplay(ContentDisplay.RIGHT);
+ textFlow.getChildren().add(label);
}
}
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/NextUnseenGroup.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/NextUnseenGroup.java
index fe12dfe010..b53c36c91e 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/NextUnseenGroup.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/NextUnseenGroup.java
@@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
- * Copyright 2011-17 Basis Technology Corp.
+ * Copyright 2011-18 Basis Technology Corp.
* Contact: carrier sleuthkit org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,15 +18,16 @@
*/
package org.sleuthkit.autopsy.imagegallery.actions;
+import com.google.common.util.concurrent.MoreExecutors;
import java.util.Optional;
+import javafx.application.Platform;
import javafx.beans.Observable;
-import javafx.beans.binding.ObjectExpression;
import javafx.collections.ObservableList;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
+import org.apache.commons.collections4.CollectionUtils;
import org.controlsfx.control.action.Action;
import org.openide.util.NbBundle;
-import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup;
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
* group
*/
-@NbBundle.Messages({"NextUnseenGroup.markGroupSeen=Mark Group Seen",
- "NextUnseenGroup.nextUnseenGroup=Next Unseen group"})
+@NbBundle.Messages({
+ "NextUnseenGroup.markGroupSeen=Mark Group Seen",
+ "NextUnseenGroup.nextUnseenGroup=Next Unseen group",
+ "NextUnseenGroup.allGroupsSeen=All Groups Have Been Seen"})
public class NextUnseenGroup extends Action {
- private static final Image END =
- new Image(NextUnseenGroup.class.getResourceAsStream("/org/sleuthkit/autopsy/imagegallery/images/control-stop.png")); //NON-NLS
- private static final Image ADVANCE =
- new Image(NextUnseenGroup.class.getResourceAsStream("/org/sleuthkit/autopsy/imagegallery/images/control-double.png")); //NON-NLS
+ private static final String IMAGE_PATH = "/org/sleuthkit/autopsy/imagegallery/images/"; //NON-NLS
+ private static final Image END_IMAGE = new Image(NextUnseenGroup.class.getResourceAsStream(
+ IMAGE_PATH + "control-stop.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 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 unSeenGroups;
- private final ObservableList analyzedGroups;
+ private final GroupManager groupManager;
public NextUnseenGroup(ImageGalleryController controller) {
super(NEXT_UNSEEN_GROUP);
+ setGraphic(new ImageView(ADVANCE_IMAGE));
+
+ this.controller = controller;
groupManager = controller.getGroupManager();
unSeenGroups = groupManager.getUnSeenGroups();
- analyzedGroups = groupManager.getAnalyzedGroups();
- setGraphic(new ImageView(ADVANCE));
+ unSeenGroups.addListener((Observable observable) -> updateButton());
+ controller.viewStateProperty().addListener((Observable observable) -> updateButton());
- //TODO: do we need both these listeners?
- analyzedGroups.addListener((Observable o) -> this.updateButton());
- unSeenGroups.addListener((Observable o) -> this.updateButton());
-
- setEventHandler(event -> {
- //fx-thread
+ setEventHandler(event -> { //on fx-thread
//if there is a group assigned to the view, mark it as seen
- Optional.ofNullable(controller.viewState())
- .map(ObjectExpression::getValue)
- .map(GroupViewState::getGroup)
- .ifPresent(group -> groupManager.markGroupSeen(group, true));
-
- if (unSeenGroups.isEmpty() == false) {
- controller.advance(GroupViewState.tile(unSeenGroups.get(0)), true);
- updateButton();
- }
+ Optional.ofNullable(controller.getViewState())
+ .flatMap(GroupViewState::getGroup)
+ .ifPresent(group -> {
+ groupManager.markGroupSeen(group, true)
+ .addListener(this::advanceToNextUnseenGroup, MoreExecutors.newDirectExecutorService());
+ });
});
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() {
- setDisabled(unSeenGroups.isEmpty());
- if (unSeenGroups.size() <= 1) {
- setText(MARK_GROUP_SEEN);
- setGraphic(new ImageView(END));
+ int size = unSeenGroups.size();
+
+ if (size < 1) {
+ //there are no unseen groups.
+ Platform.runLater(() -> {
+ setDisabled(true);
+ setText(ALL_GROUPS_SEEN);
+ setGraphic(null);
+ });
} else {
- setText(NEXT_UNSEEN_GROUP);
- setGraphic(new ImageView(ADVANCE));
+ 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);
+ setGraphic(new ImageView(ADVANCE_IMAGE));
+ });
+ }
}
}
}
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java
index e10a679792..efbdc3de00 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java
@@ -21,16 +21,11 @@ package org.sleuthkit.autopsy.imagegallery.actions;
import java.awt.Component;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
-import java.io.IOException;
-import java.net.URL;
import java.util.logging.Level;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
-import javafx.scene.control.Dialog;
-import javafx.scene.image.Image;
import javafx.stage.Modality;
-import javafx.stage.Stage;
import javax.swing.ImageIcon;
import javax.swing.JButton;
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.RuntimeProperties;
import org.sleuthkit.autopsy.coreutils.Logger;
-import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryModule;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryTopComponent;
+import org.sleuthkit.autopsy.imagegallery.gui.GuiUtils;
import org.sleuthkit.datamodel.TskCoreException;
@ActionID(category = "Tools", id = "org.sleuthkit.autopsy.imagegallery.OpenAction")
@ActionReferences(value = {
- @ActionReference(path = "Menu/Tools", position = 101),
+ @ActionReference(path = "Menu/Tools", position = 101)
+ ,
@ActionReference(path = "Toolbars/Case", position = 101)})
@ActionRegistration(displayName = "#CTL_OpenAction", lazy = false)
@Messages({"CTL_OpenAction=Images/Videos",
- "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" +
- "Choosing 'yes' will update the database and enable listening to future ingests.",
+ "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"
+ + "Choosing 'yes' will update the database and enable listening to future ingests.",
"OpenAction.stale.confDlg.title=Image Gallery"})
public final class OpenAction extends CallableSystemAction {
private static final Logger logger = Logger.getLogger(OpenAction.class.getName());
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 final PropertyChangeListener pcl;
@@ -145,10 +125,7 @@ public final class OpenAction extends CallableSystemAction {
}
@Override
- @SuppressWarnings("fallthrough")
- @NbBundle.Messages({
- "OpenAction.dialogTitle=Image Gallery"
- })
+ @NbBundle.Messages({"OpenAction.dialogTitle=Image Gallery"})
public void performAction() {
//check case
@@ -165,24 +142,49 @@ public final class OpenAction extends CallableSystemAction {
setEnabled(false);
return;
}
- if (ImageGalleryModule.isDrawableDBStale(currentCase)) {
- //drawable db is stale, ask what to do
- int answer = JOptionPane.showConfirmDialog(WindowManager.getDefault().getMainWindow(), Bundle.OpenAction_stale_confDlg_msg(),
- Bundle.OpenAction_stale_confDlg_title(), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE);
+ try {
+ ImageGalleryController controller = ImageGalleryModule.getController();
+ if (controller.isDataSourcesTableStale()) {
+ //drawable db is stale, ask what to do
+ int answer = JOptionPane.showConfirmDialog(
+ WindowManager.getDefault().getMainWindow(),
+ Bundle.OpenAction_stale_confDlg_msg(),
+ Bundle.OpenAction_stale_confDlg_title(),
+ JOptionPane.YES_NO_CANCEL_OPTION,
+ JOptionPane.WARNING_MESSAGE);
- switch (answer) {
- case JOptionPane.YES_OPTION:
- ImageGalleryController.getDefault().setListeningEnabled(true);
- //fall through
- case JOptionPane.NO_OPTION:
- ImageGalleryTopComponent.openTopComponent();
+ switch (answer) {
+ case JOptionPane.YES_OPTION:
+ /* For a single-user case, we favor user experience, and
+ * rebuild the database as soon as Image Gallery is
+ * 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();
+ break;
+
+ case JOptionPane.NO_OPTION: {
+ ImageGalleryTopComponent.openTopComponent();
+ }
break;
- case JOptionPane.CANCEL_OPTION:
- break; //do nothing
+ case JOptionPane.CANCEL_OPTION:
+ break; //do nothing
+ }
+ } else {
+ //drawable db is not stale, just open it
+ ImageGalleryTopComponent.openTopComponent();
}
- } else {
- //drawable db is not stale, just open it
- ImageGalleryTopComponent.openTopComponent();
+ } catch (NoCurrentCaseException noCurrentCaseException) {
+ logger.log(Level.WARNING, "There was no case open when Image Gallery was opened.", noCurrentCaseException);
}
}
@@ -198,16 +200,6 @@ public final class OpenAction extends CallableSystemAction {
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({
"ImageGallery.showTooManyFiles.contentText="
+ "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);
dialog.initModality(Modality.APPLICATION_MODAL);
dialog.setTitle(Bundle.OpenAction_dialogTitle());
- setDialogIcons(dialog);
+ GuiUtils.setDialogIcons(dialog);
dialog.setHeaderText(Bundle.ImageGallery_showTooManyFiles_headerText());
dialog.showAndWait();
}
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/TagGroupAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/TagGroupAction.java
index afd6a7dcfc..d48349ac22 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/TagGroupAction.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/TagGroupAction.java
@@ -29,9 +29,10 @@ public class TagGroupAction extends AddTagAction {
public TagGroupAction(final TagName tagName, ImageGalleryController controller) {
super(controller, tagName, null);
- setEventHandler(actionEvent ->
- new AddTagAction(controller, tagName, ImmutableSet.copyOf(controller.viewState().get().getGroup().getFileIDs())).
- handle(actionEvent)
- );
+ setEventHandler(actionEvent -> {
+ controller.getViewState().getGroup().ifPresent(group -> {
+ new AddTagAction(controller, tagName, ImmutableSet.copyOf(group.getFileIDs())).handle(actionEvent);
+ });
+ });
}
}
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/CategoryManager.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/CategoryManager.java
index 66aa6c578d..5168e25630 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/CategoryManager.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/CategoryManager.java
@@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
- * Copyright 2015-16 Basis Technology Corp.
+ * Copyright 2015-18 Basis Technology Corp.
* Contact: carrier sleuthkit org
*
* 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.ContentTagDeletedEvent;
import org.sleuthkit.autopsy.coreutils.Logger;
-import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
+import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.datamodel.ContentTag;
import org.sleuthkit.datamodel.TagName;
import org.sleuthkit.datamodel.TskCoreException;
-
/**
* 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
- * implement a public method annotated with {@link Subscribe} that accepts one
- * argument of type CategoryChangeEvent
+ * implement a public method annotated with Subscribe that accepts one argument
+ * of type CategoryChangeEvent
*
* TODO: currently these two functions (cached counts and events) are separate
* 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
* 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(
- new BasicThreadFactory.Builder().namingPattern("Category Event Bus").uncaughtExceptionHandler((Thread t, Throwable e) -> { //NON-NLS
- LOGGER.log(Level.SEVERE, "Uncaught exception in category event bus handler", 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", throwable); //NON-NLS
}).build()
));
@@ -80,38 +79,29 @@ public class CategoryManager {
* the count related methods go through this cache, which loads initial
* values from the database if needed.
*/
- private final LoadingCache categoryCounts =
- CacheBuilder.newBuilder().build(CacheLoader.from(this::getCategoryCountHelper));
+ private final LoadingCache categoryCounts
+ = CacheBuilder.newBuilder().build(CacheLoader.from(this::getCategoryCountHelper));
/**
* cached TagNames corresponding to Categories, looked up from
* autopsyTagManager at initial request or if invalidated by case change.
*/
- private final LoadingCache catTagNameMap =
- CacheBuilder.newBuilder().build(CacheLoader.from(
- cat -> getController().getTagsManager().getTagName(cat)
- ));
+ private final LoadingCache catTagNameMap
+ = CacheBuilder.newBuilder().build(new CacheLoader() {
+ @Override
+ public TagName load(DhsImageCategory cat) throws TskCoreException {
+ return getController().getTagsManager().getTagName(cat);
+ }
+ });
public CategoryManager(ImageGalleryController controller) {
this.controller = controller;
+ this.drawableDb = controller.getDatabase();
}
private ImageGalleryController getController() {
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() {
categoryCounts.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
// 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);
- return db.getNumberOfImageFilesInList() - allOtherCatCount;
+ return drawableDb.getNumberOfImageFilesInList() - allOtherCatCount;
} else {
return categoryCounts.getUnchecked(cat).sum();
}
@@ -151,7 +141,7 @@ public class CategoryManager {
/**
* decrement the cached value for the number of files with the given
- * {@link DhsImageCategory}
+ * DhsImageCategory
*
* @param cat the Category to decrement
*/
@@ -175,7 +165,7 @@ public class CategoryManager {
LongAdder longAdder = new LongAdder();
longAdder.decrement();
try {
- longAdder.add(db.getCategoryCount(cat));
+ longAdder.add(drawableDb.getCategoryCount(cat));
longAdder.increment();
} catch (IllegalStateException ex) {
LOGGER.log(Level.WARNING, "Case closed while getting files"); //NON-NLS
@@ -212,15 +202,14 @@ public class CategoryManager {
try {
categoryEventBus.unregister(listener);
} 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
- * when the groups should all be registered. To avoid cluttering
- * the logs we have disabled recording this exception. This
- * documented in 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 {
+ /*
+ * We don't fully understand why we are getting this exception when
+ * the groups should all be registered. To avoid cluttering the logs
+ * we have disabled recording this exception. This documented in
+ * issues 738 and 802.
+ */
+
+ if (!e.getMessage().contains("missing event subscriber for an annotated method. Is " + listener + " registered?")) { //NON-NLS
throw e;
}
}
@@ -258,7 +247,7 @@ public class CategoryManager {
//remove old category tag(s) if necessary
for (ContentTag ct : tagsManager.getContentTags(addedTag.getContent())) {
if (ct.getId() != addedTag.getId()
- && CategoryManager.isCategoryTagName(ct.getName())) {
+ && CategoryManager.isCategoryTagName(ct.getName())) {
try {
tagsManager.deleteContentTag(ct);
} catch (TskCoreException tskException) {
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java
index 7688ccd146..f779978dfb 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java
@@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
- * Copyright 2013-16 Basis Technology Corp.
+ * Copyright 2013-2018 Basis Technology Corp.
* Contact: carrier sleuthkit org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,7 +18,8 @@
*/
package org.sleuthkit.autopsy.imagegallery.datamodel;
-import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -38,7 +39,9 @@ import java.util.Map;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import java.util.Set;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
@@ -46,9 +49,11 @@ import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.GuardedBy;
import javax.swing.SortOrder;
+import static org.apache.commons.lang3.ObjectUtils.notEqual;
import org.apache.commons.lang3.StringUtils;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.imagegallery.FileTypeUtils;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
import org.sleuthkit.autopsy.imagegallery.ImageGalleryModule;
@@ -59,24 +64,26 @@ import static org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupSortBy.
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.BlackboardArtifact;
import org.sleuthkit.datamodel.BlackboardAttribute;
+import org.sleuthkit.datamodel.CaseDbAccessManager.CaseDbAccessQueryCallback;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.ContentTag;
+import org.sleuthkit.datamodel.DataSource;
import org.sleuthkit.datamodel.SleuthkitCase;
+import org.sleuthkit.datamodel.SleuthkitCase.CaseDbTransaction;
import org.sleuthkit.datamodel.TagName;
import org.sleuthkit.datamodel.TskCoreException;
+import org.sleuthkit.datamodel.TskData.DbType;
+import org.sleuthkit.datamodel.TskDataException;
import org.sqlite.SQLiteJDBCLoader;
/**
- * This class is the public interface to the Image / Video Analyzer SQLite
- * database. This class borrows a lot of ideas and techniques (for good or ill)
- * from {@link SleuthkitCase}.
- *
- * TODO: Creating an abstract base class for sqlite databases may make sense in
- * the future. see also {@link EventsDB} in the timeline viewer.
+ * This class is the public interface to the Image Gallery SQLite database. This
+ * class borrows a lot of ideas and techniques (for good or ill) from
+ * SleuthkitCase
*/
public final class DrawableDB {
- private static final org.sleuthkit.autopsy.coreutils.Logger LOGGER = Logger.getLogger(DrawableDB.class.getName());
+ private static final Logger logger = Logger.getLogger(DrawableDB.class.getName());
//column name constants//////////////////////
private static final String ANALYZED = "analyzed"; //NON-NLS
@@ -85,24 +92,23 @@ public final class DrawableDB {
private static final String HASH_SET_NAME = "hash_set_name"; //NON-NLS
+ private static final String GROUPS_TABLENAME = "image_gallery_groups"; //NON-NLS
+ private static final String GROUPS_SEEN_TABLENAME = "image_gallery_groups_seen"; //NON-NLS
+
private final PreparedStatement insertHashSetStmt;
- private final PreparedStatement groupSeenQueryStmt;
-
- private final PreparedStatement insertGroupStmt;
-
private final List preparedStatements = new ArrayList<>();
private final PreparedStatement removeFileStmt;
- private final PreparedStatement updateGroupStmt;
-
private final PreparedStatement selectHashSetStmt;
private final PreparedStatement selectHashSetNamesStmt;
private final PreparedStatement insertHashHitStmt;
+ private final PreparedStatement updateDataSourceStmt;
+
private final PreparedStatement updateFileStmt;
private final PreparedStatement insertFileStmt;
@@ -122,11 +128,14 @@ public final class DrawableDB {
private final PreparedStatement hashSetGroupStmt;
+ private final PreparedStatement pathGroupFilterByDataSrcStmt;
+
/**
- * map from {@link DrawableAttribute} to the {@link PreparedStatement} thet
+ * map from {@link DrawableAttribute} to the {@link PreparedStatement} that
* is used to select groups for that attribute
*/
private final Map, PreparedStatement> groupStatementMap = new HashMap<>();
+ private final Map, PreparedStatement> groupStatementFilterByDataSrcMap = new HashMap<>();
private final GroupManager groupManager;
@@ -142,12 +151,21 @@ public final class DrawableDB {
try {
Class.forName("org.sqlite.JDBC");
} catch (ClassNotFoundException ex) {
- LOGGER.log(Level.SEVERE, "Failed to load sqlite JDBC driver", ex); //NON-NLS
+ logger.log(Level.SEVERE, "Failed to load sqlite JDBC driver", ex); //NON-NLS
}
}
private final SleuthkitCase tskCase;
private final ImageGalleryController controller;
+ /**
+ * Enum to track Image gallery db rebuild status for a data source
+ */
+ public enum DrawableDbBuildStatusEnum {
+ UNKNOWN, /// no known status
+ IN_PROGRESS, /// drawable db rebuild has been started for the data source
+ COMPLETE; /// drawable db rebuild is complete for the data source
+ }
+
//////////////general database logic , mostly borrowed from sleuthkitcase
/**
* Lock to protect against concurrent write accesses to case database and to
@@ -195,7 +213,7 @@ public final class DrawableDB {
*
* @throws SQLException if there is problem creating or configuring the db
*/
- private DrawableDB(Path dbPath, ImageGalleryController controller) throws SQLException, ExceptionInInitializerError, IOException {
+ private DrawableDB(Path dbPath, ImageGalleryController controller) throws TskCoreException, SQLException, IOException {
this.dbPath = dbPath;
this.controller = controller;
this.tskCase = controller.getSleuthKitCase();
@@ -203,11 +221,15 @@ public final class DrawableDB {
Files.createDirectories(dbPath.getParent());
if (initializeDBSchema()) {
updateFileStmt = prepareStatement(
- "INSERT OR REPLACE INTO drawable_files (obj_id , path, name, created_time, modified_time, make, model, analyzed) " //NON-NLS
- + "VALUES (?,?,?,?,?,?,?,?)"); //NON-NLS
+ "INSERT OR REPLACE INTO drawable_files (obj_id, data_source_obj_id, path, name, created_time, modified_time, make, model, analyzed) " //NON-NLS
+ + "VALUES (?,?,?,?,?,?,?,?,?)"); //NON-NLS
insertFileStmt = prepareStatement(
- "INSERT OR IGNORE INTO drawable_files (obj_id , path, name, created_time, modified_time, make, model, analyzed) " //NON-NLS
- + "VALUES (?,?,?,?,?,?,?,?)"); //NON-NLS
+ "INSERT OR IGNORE INTO drawable_files (obj_id, data_source_obj_id, path, name, created_time, modified_time, make, model, analyzed) " //NON-NLS
+ + "VALUES (?,?,?,?,?,?,?,?,?)"); //NON-NLS
+
+ updateDataSourceStmt = prepareStatement(
+ "INSERT OR REPLACE INTO datasources (ds_obj_id, drawable_db_build_status) " //NON-NLS
+ + " VALUES (?,?)"); //NON-NLS
removeFileStmt = prepareStatement("DELETE FROM drawable_files WHERE obj_id = ?"); //NON-NLS
@@ -220,10 +242,8 @@ public final class DrawableDB {
analyzedGroupStmt = prepareStatement("SELECT obj_id , analyzed FROM drawable_files WHERE analyzed = ?", DrawableAttribute.ANALYZED); //NON-NLS
hashSetGroupStmt = prepareStatement("SELECT drawable_files.obj_id AS obj_id, analyzed FROM drawable_files , hash_sets , hash_set_hits WHERE drawable_files.obj_id = hash_set_hits.obj_id AND hash_sets.hash_set_id = hash_set_hits.hash_set_id AND hash_sets.hash_set_name = ?", DrawableAttribute.HASHSET); //NON-NLS
- updateGroupStmt = prepareStatement("insert or replace into groups (seen, value, attribute) values( ?, ? , ?)"); //NON-NLS
- insertGroupStmt = prepareStatement("insert or ignore into groups (value, attribute) values (?,?)"); //NON-NLS
-
- groupSeenQueryStmt = prepareStatement("SELECT seen FROM groups WHERE value = ? AND attribute = ?"); //NON-NLS
+ //add other xyzFilterByDataSrc prepared statments as we add support for filtering by DS to other groups
+ pathGroupFilterByDataSrcStmt = prepareFilterByDataSrcStatement("SELECT obj_id , analyzed FROM drawable_files WHERE path = ? AND data_source_obj_id = ?", DrawableAttribute.PATH);
selectHashSetNamesStmt = prepareStatement("SELECT DISTINCT hash_set_name FROM hash_sets"); //NON-NLS
insertHashSetStmt = prepareStatement("INSERT OR IGNORE INTO hash_sets (hash_set_name) VALUES (?)"); //NON-NLS
@@ -231,14 +251,28 @@ public final class DrawableDB {
insertHashHitStmt = prepareStatement("INSERT OR IGNORE INTO hash_set_hits (hash_set_id, obj_id) VALUES (?,?)"); //NON-NLS
- for (DhsImageCategory cat : DhsImageCategory.values()) {
- insertGroup(cat.getDisplayName(), DrawableAttribute.CATEGORY);
+ CaseDbTransaction caseDbTransaction = null;
+ try {
+ caseDbTransaction = tskCase.beginTransaction();
+ for (DhsImageCategory cat : DhsImageCategory.values()) {
+ insertGroup(cat.getDisplayName(), DrawableAttribute.CATEGORY, caseDbTransaction);
+ }
+ caseDbTransaction.commit();
+ } catch (TskCoreException ex) {
+ if (null != caseDbTransaction) {
+ try {
+ caseDbTransaction.rollback();
+ } catch (TskCoreException ex2) {
+ logger.log(Level.SEVERE, "Error in trying to rollback transaction", ex2);
+ }
+ }
+ throw ex;
}
+
initializeImageList();
} else {
- throw new ExceptionInInitializerError();
+ throw new TskCoreException("Failed to initialize Image Gallery db schema");
}
-
}
/**
@@ -281,23 +315,55 @@ public final class DrawableDB {
}
/**
- * public factory method. Creates and opens a connection to a new database *
- * at the given path.
+ * calls {@link DrawableDB#prepareStatement(java.lang.String) ,
+ * and then add the statement to the groupStatementFilterByDataSrcMap map used to lookup
+ * statements by the attribute/column they group on
*
- * @param dbPath
+ * @param stmtString the string representation of the sqlite statement to
+ * prepare
+ * @param attr the {@link DrawableAttribute} this query groups by
+ * *
+ * @return the prepared statement
*
- * @return
+ * @throws SQLExceptionif unable to prepare the statement
*/
- public static DrawableDB getDrawableDB(Path dbPath, ImageGalleryController controller) {
+ private PreparedStatement prepareFilterByDataSrcStatement(String stmtString, DrawableAttribute> attr) throws SQLException {
+ PreparedStatement prepareStatement = prepareStatement(stmtString);
+ if (attr != null) {
+ groupStatementFilterByDataSrcMap.put(attr, prepareStatement);
+ }
+ return prepareStatement;
+ }
+
+ private void setQueryParams(PreparedStatement statement, GroupKey> groupKey) throws SQLException {
+
+ statement.setObject(1, groupKey.getValue());
+
+ if (groupKey.getDataSource().isPresent()
+ && (groupKey.getAttribute() == DrawableAttribute.PATH)) {
+ statement.setObject(2, groupKey.getDataSourceObjId());
+ }
+ }
+
+ /**
+ * public factory method. Creates and opens a connection to a new database *
+ * at the given path. *
+ *
+ * @param controller
+ *
+ * @return A DrawableDB for the given controller.
+ *
+ * @throws org.sleuthkit.datamodel.TskCoreException
+ */
+ public static DrawableDB getDrawableDB(ImageGalleryController controller) throws TskCoreException {
+ Path dbPath = ImageGalleryModule.getModuleOutputDir(controller.getAutopsyCase());
try {
return new DrawableDB(dbPath.resolve("drawable.db"), controller); //NON-NLS
} catch (SQLException ex) {
- LOGGER.log(Level.SEVERE, "sql error creating database connection", ex); //NON-NLS
- return null;
- } catch (ExceptionInInitializerError | IOException ex) {
- LOGGER.log(Level.SEVERE, "error creating database connection", ex); //NON-NLS
- return null;
+ throw new TskCoreException("sql error creating database connection", ex); //NON-NLS
+ } catch (IOException ex) {
+ throw new TskCoreException("Error creating database connection", ex); //NON-NLS
}
}
@@ -328,11 +394,11 @@ public final class DrawableDB {
}
try {
- LOGGER.log(Level.INFO, String.format("sqlite-jdbc version %s loaded in %s mode", //NON-NLS
+ logger.log(Level.INFO, String.format("sqlite-jdbc version %s loaded in %s mode", //NON-NLS
SQLiteJDBCLoader.getVersion(), SQLiteJDBCLoader.isNativeMode()
- ? "native" : "pure-java")); //NON-NLS
+ ? "native" : "pure-java")); //NON-NLS
} catch (Exception exception) {
- LOGGER.log(Level.WARNING, "exception while checking sqlite-jdbc version and mode", exception); //NON-NLS
+ logger.log(Level.WARNING, "exception while checking sqlite-jdbc version and mode", exception); //NON-NLS
}
}
@@ -351,56 +417,92 @@ public final class DrawableDB {
setPragmas();
} catch (SQLException ex) {
- LOGGER.log(Level.SEVERE, "problem accessing database", ex); //NON-NLS
- return false;
- }
- try (Statement stmt = con.createStatement()) {
- String sql = "CREATE TABLE if not exists drawable_files " //NON-NLS
- + "( obj_id INTEGER PRIMARY KEY, " //NON-NLS
- + " path VARCHAR(255), " //NON-NLS
- + " name VARCHAR(255), " //NON-NLS
- + " created_time integer, " //NON-NLS
- + " modified_time integer, " //NON-NLS
- + " make VARCHAR(255), " //NON-NLS
- + " model VARCHAR(255), " //NON-NLS
- + " analyzed integer DEFAULT 0)"; //NON-NLS
- stmt.execute(sql);
- } catch (SQLException ex) {
- LOGGER.log(Level.SEVERE, "problem creating drawable_files table", ex); //NON-NLS
+ logger.log(Level.SEVERE, "problem accessing database", ex); //NON-NLS
return false;
}
try (Statement stmt = con.createStatement()) {
- String sql = "CREATE TABLE if not exists groups " //NON-NLS
- + "(group_id INTEGER PRIMARY KEY, " //NON-NLS
- + " value VARCHAR(255) not null, " //NON-NLS
- + " attribute VARCHAR(255) not null, " //NON-NLS
- + " seen integer DEFAULT 0, " //NON-NLS
- + " UNIQUE(value, attribute) )"; //NON-NLS
+ String sql = "CREATE TABLE IF NOT EXISTS datasources " //NON-NLS
+ + "( id INTEGER PRIMARY KEY, " //NON-NLS
+ + " ds_obj_id integer UNIQUE NOT NULL, "
+ + " drawable_db_build_status VARCHAR(128) )"; //NON-NLS
stmt.execute(sql);
} catch (SQLException ex) {
- LOGGER.log(Level.SEVERE, "problem creating groups table", ex); //NON-NLS
+ logger.log(Level.SEVERE, "problem creating datasources table", ex); //NON-NLS
+ return false;
+ }
+
+ try (Statement stmt = con.createStatement()) {
+ String sql = "CREATE TABLE if not exists drawable_files " //NON-NLS
+ + "( obj_id INTEGER PRIMARY KEY, " //NON-NLS
+ + " data_source_obj_id INTEGER NOT NULL, "
+ + " path VARCHAR(255), " //NON-NLS
+ + " name VARCHAR(255), " //NON-NLS
+ + " created_time integer, " //NON-NLS
+ + " modified_time integer, " //NON-NLS
+ + " make VARCHAR(255), " //NON-NLS
+ + " model VARCHAR(255), " //NON-NLS
+ + " analyzed integer DEFAULT 0)"; //NON-NLS
+ stmt.execute(sql);
+ } catch (SQLException ex) {
+ logger.log(Level.SEVERE, "problem creating drawable_files table", ex); //NON-NLS
+ return false;
+ }
+
+ String autogenKeyType = (DbType.POSTGRESQL == tskCase.getDatabaseType()) ? "BIGSERIAL" : "INTEGER";
+
+ // The image_gallery_groups table is created in the Case Database
+ try {
+ String tableSchema
+ = "( group_id " + autogenKeyType + " PRIMARY KEY, " //NON-NLS
+ + " data_source_obj_id integer DEFAULT 0, "
+ + " value VARCHAR(255) not null, " //NON-NLS
+ + " attribute VARCHAR(255) not null, " //NON-NLS
+ + " UNIQUE(data_source_obj_id, value, attribute) )"; //NON-NLS
+
+ tskCase.getCaseDbAccessManager().createTable(GROUPS_TABLENAME, tableSchema);
+ } catch (TskCoreException ex) {
+ logger.log(Level.SEVERE, "problem creating groups table", ex); //NON-NLS
+ return false;
+ }
+
+ // The image_gallery_groups_seen table is created in the Case Database
+ try {
+
+ String tableSchema
+ = "( id " + autogenKeyType + " PRIMARY KEY, " //NON-NLS
+ + " group_id integer not null, " //NON-NLS
+ + " examiner_id integer not null, " //NON-NLS
+ + " seen integer DEFAULT 0, " //NON-NLS
+ + " UNIQUE(group_id, examiner_id),"
+ + " FOREIGN KEY(group_id) REFERENCES " + GROUPS_TABLENAME + "(group_id),"
+ + " FOREIGN KEY(examiner_id) REFERENCES tsk_examiners(examiner_id)"
+ + " )"; //NON-NLS
+
+ tskCase.getCaseDbAccessManager().createTable(GROUPS_SEEN_TABLENAME, tableSchema);
+ } catch (TskCoreException ex) {
+ logger.log(Level.SEVERE, "problem creating image_gallery_groups_seen table", ex); //NON-NLS
return false;
}
try (Statement stmt = con.createStatement()) {
String sql = "CREATE TABLE if not exists hash_sets " //NON-NLS
- + "( hash_set_id INTEGER primary key," //NON-NLS
- + " hash_set_name VARCHAR(255) UNIQUE NOT NULL)"; //NON-NLS
+ + "( hash_set_id INTEGER primary key," //NON-NLS
+ + " hash_set_name VARCHAR(255) UNIQUE NOT NULL)"; //NON-NLS
stmt.execute(sql);
} catch (SQLException ex) {
- LOGGER.log(Level.SEVERE, "problem creating hash_sets table", ex); //NON-NLS
+ logger.log(Level.SEVERE, "problem creating hash_sets table", ex); //NON-NLS
return false;
}
try (Statement stmt = con.createStatement()) {
String sql = "CREATE TABLE if not exists hash_set_hits " //NON-NLS
- + "(hash_set_id INTEGER REFERENCES hash_sets(hash_set_id) not null, " //NON-NLS
- + " obj_id INTEGER REFERENCES drawable_files(obj_id) not null, " //NON-NLS
- + " PRIMARY KEY (hash_set_id, obj_id))"; //NON-NLS
+ + "(hash_set_id INTEGER REFERENCES hash_sets(hash_set_id) not null, " //NON-NLS
+ + " obj_id INTEGER REFERENCES drawable_files(obj_id) not null, " //NON-NLS
+ + " PRIMARY KEY (hash_set_id, obj_id))"; //NON-NLS
stmt.execute(sql);
} catch (SQLException ex) {
- LOGGER.log(Level.SEVERE, "problem creating hash_set_hits table", ex); //NON-NLS
+ logger.log(Level.SEVERE, "problem creating hash_set_hits table", ex); //NON-NLS
return false;
}
@@ -408,35 +510,35 @@ public final class DrawableDB {
String sql = "CREATE INDEX if not exists path_idx ON drawable_files(path)"; //NON-NLS
stmt.execute(sql);
} catch (SQLException ex) {
- LOGGER.log(Level.WARNING, "problem creating path_idx", ex); //NON-NLS
+ logger.log(Level.WARNING, "problem creating path_idx", ex); //NON-NLS
}
try (Statement stmt = con.createStatement()) {
String sql = "CREATE INDEX if not exists name_idx ON drawable_files(name)"; //NON-NLS
stmt.execute(sql);
} catch (SQLException ex) {
- LOGGER.log(Level.WARNING, "problem creating name_idx", ex); //NON-NLS
+ logger.log(Level.WARNING, "problem creating name_idx", ex); //NON-NLS
}
try (Statement stmt = con.createStatement()) {
String sql = "CREATE INDEX if not exists make_idx ON drawable_files(make)"; //NON-NLS
stmt.execute(sql);
} catch (SQLException ex) {
- LOGGER.log(Level.WARNING, "problem creating make_idx", ex); //NON-NLS
+ logger.log(Level.WARNING, "problem creating make_idx", ex); //NON-NLS
}
try (Statement stmt = con.createStatement()) {
String sql = "CREATE INDEX if not exists model_idx ON drawable_files(model)"; //NON-NLS
stmt.execute(sql);
} catch (SQLException ex) {
- LOGGER.log(Level.WARNING, "problem creating model_idx", ex); //NON-NLS
+ logger.log(Level.WARNING, "problem creating model_idx", ex); //NON-NLS
}
try (Statement stmt = con.createStatement()) {
String sql = "CREATE INDEX if not exists analyzed_idx ON drawable_files(analyzed)"; //NON-NLS
stmt.execute(sql);
} catch (SQLException ex) {
- LOGGER.log(Level.WARNING, "problem creating analyzed_idx", ex); //NON-NLS
+ logger.log(Level.WARNING, "problem creating analyzed_idx", ex); //NON-NLS
}
return true;
@@ -457,7 +559,7 @@ public final class DrawableDB {
closeStatements();
con.close();
} catch (SQLException ex) {
- LOGGER.log(Level.WARNING, "Failed to close connection to drawable.db", ex); //NON-NLS
+ logger.log(Level.WARNING, "Failed to close connection to drawable.db", ex); //NON-NLS
}
}
con = null;
@@ -469,7 +571,7 @@ public final class DrawableDB {
con = DriverManager.getConnection("jdbc:sqlite:" + dbPath.toString()); //NON-NLS
}
} catch (SQLException ex) {
- LOGGER.log(Level.WARNING, "Failed to open connection to drawable.db", ex); //NON-NLS
+ logger.log(Level.WARNING, "Failed to open connection to drawable.db", ex); //NON-NLS
}
}
@@ -520,47 +622,107 @@ public final class DrawableDB {
names.add(rs.getString(HASH_SET_NAME));
}
} catch (SQLException sQLException) {
- LOGGER.log(Level.WARNING, "failed to get hash set names", sQLException); //NON-NLS
+ logger.log(Level.WARNING, "failed to get hash set names", sQLException); //NON-NLS
} finally {
dbReadUnlock();
}
return names;
}
+ static private String getGroupIdQuery(GroupKey> groupKey) {
+ // query to find the group id from attribute/value
+ return String.format(" SELECT group_id FROM " + GROUPS_TABLENAME
+ + " WHERE attribute = \'%s\' AND value = \'%s\' AND data_source_obj_id = %d",
+ groupKey.getAttribute().attrName.toString(),
+ groupKey.getValueDisplayName(),
+ (groupKey.getAttribute() == DrawableAttribute.PATH) ? groupKey.getDataSourceObjId() : 0);
+ }
+
+ /**
+ * Returns true if the specified group has been any examiner
+ *
+ * @param groupKey
+ *
+ * @return
+ */
public boolean isGroupSeen(GroupKey> groupKey) {
- dbReadLock();
- try {
- groupSeenQueryStmt.clearParameters();
- groupSeenQueryStmt.setString(1, groupKey.getValueDisplayName());
- groupSeenQueryStmt.setString(2, groupKey.getAttribute().attrName.toString());
- try (ResultSet rs = groupSeenQueryStmt.executeQuery()) {
- while (rs.next()) {
- return rs.getBoolean("seen"); //NON-NLS
+ return isGroupSeenByExaminer(groupKey, -1);
+ }
+
+ /**
+ * Returns true if the specified group has been seen by the specified
+ * examiner
+ *
+ * @param groupKey - key to identify the group
+ * @param examinerId
+ *
+ * @return true if the examine has this group, false otherwise
+ */
+ public boolean isGroupSeenByExaminer(GroupKey> groupKey, long examinerId) {
+
+ // Callback to process result of seen query
+ class GroupSeenQueryResultProcessor extends CompletableFuture implements CaseDbAccessQueryCallback {
+
+ @Override
+ public void process(ResultSet resultSet) {
+ try {
+ if (resultSet != null) {
+ while (resultSet.next()) {
+ complete(resultSet.getInt("count") > 0); //NON-NLS;
+ return;
+ }
+ }
+ } catch (SQLException ex) {
+ logger.log(Level.SEVERE, "Failed to get group seen", ex); //NON-NLS
}
}
- } catch (SQLException ex) {
- String msg = String.format("Failed to get is group seen for group key %s", groupKey.getValueDisplayName()); //NON-NLS
- LOGGER.log(Level.WARNING, msg, ex);
- } finally {
- dbReadUnlock();
}
+ // Callback to process result of seen query
+ GroupSeenQueryResultProcessor queryResultProcessor = new GroupSeenQueryResultProcessor();
+
+ try {
+ String groupSeenQueryStmt = "COUNT(*) as count FROM " + GROUPS_SEEN_TABLENAME
+ + " WHERE seen = 1 "
+ + " AND group_id in ( " + getGroupIdQuery(groupKey) + ")"
+ + (examinerId > 0 ? " AND examiner_id = " + examinerId : "");// query to find the group id from attribute/value
+
+ tskCase.getCaseDbAccessManager().select(groupSeenQueryStmt, queryResultProcessor);
+ return queryResultProcessor.get();
+ } catch (ExecutionException | InterruptedException | TskCoreException ex) {
+ String msg = String.format("Failed to get is group seen for group key %s", groupKey.getValueDisplayName()); //NON-NLS
+ logger.log(Level.WARNING, msg, ex);
+ }
+
return false;
}
- public void markGroupSeen(GroupKey> gk, boolean seen) {
- dbWriteLock();
- try {
- //PreparedStatement updateGroup = con.prepareStatement("update groups set seen = ? where value = ? and attribute = ?");
- updateGroupStmt.clearParameters();
- updateGroupStmt.setBoolean(1, seen);
- updateGroupStmt.setString(2, gk.getValueDisplayName());
- updateGroupStmt.setString(3, gk.getAttribute().attrName.toString());
- updateGroupStmt.execute();
- } catch (SQLException ex) {
- LOGGER.log(Level.SEVERE, "Error marking group as seen", ex); //NON-NLS
- } finally {
- dbWriteUnlock();
+ /**
+ * Record in the DB that the group with the given key has the given seen
+ * state for the given examiner id.
+ *
+ * @param groupKey
+ * @param seen
+ * @param examinerID
+ *
+ * @throws TskCoreException
+ */
+ public void markGroupSeen(GroupKey> groupKey, boolean seen, long examinerID) throws TskCoreException {
+
+ // query to find the group id from attribute/value
+ String innerQuery = String.format("( SELECT group_id FROM " + GROUPS_TABLENAME
+ + " WHERE attribute = \'%s\' AND value = \'%s\' and data_source_obj_id = %d )",
+ groupKey.getAttribute().attrName.toString(),
+ groupKey.getValueDisplayName(),
+ groupKey.getAttribute() == DrawableAttribute.PATH ? groupKey.getDataSourceObjId() : 0);
+
+ String insertSQL = String.format(" (group_id, examiner_id, seen) VALUES (%s, %d, %d)", innerQuery, examinerID, seen ? 1 : 0);
+
+ if (DbType.POSTGRESQL == tskCase.getDatabaseType()) {
+ insertSQL += String.format(" ON CONFLICT (group_id, examiner_id) DO UPDATE SET seen = %d", seen ? 1 : 0);
}
+
+ tskCase.getCaseDbAccessManager().insertOrUpdate(GROUPS_SEEN_TABLENAME, insertSQL);
+
}
public boolean removeFile(long id) {
@@ -571,23 +733,38 @@ public final class DrawableDB {
}
public void updateFile(DrawableFile f) {
- DrawableTransaction trans = beginTransaction();
- updateFile(f, trans);
- commitTransaction(trans, true);
+ DrawableTransaction trans = null;
+ CaseDbTransaction caseDbTransaction = null;
+
+ try {
+ trans = beginTransaction();
+ caseDbTransaction = tskCase.beginTransaction();
+ updateFile(f, trans, caseDbTransaction);
+ caseDbTransaction.commit();
+ commitTransaction(trans, true);
+
+ } catch (TskCoreException ex) {
+ if (null != caseDbTransaction) {
+ try {
+ caseDbTransaction.rollback();
+ } catch (TskCoreException ex2) {
+ logger.log(Level.SEVERE, "Error in trying to rollback transaction", ex2); //NON-NLS
+ }
+ }
+ if (null != trans) {
+ rollbackTransaction(trans);
+ }
+ logger.log(Level.SEVERE, "Error updating file", ex); //NON-NLS
+ }
+
}
- public void insertFile(DrawableFile f) {
- DrawableTransaction trans = beginTransaction();
- insertFile(f, trans);
- commitTransaction(trans, true);
+ public void insertFile(DrawableFile f, DrawableTransaction tr, CaseDbTransaction caseDbTransaction) {
+ insertOrUpdateFile(f, tr, insertFileStmt, caseDbTransaction);
}
- public void insertFile(DrawableFile f, DrawableTransaction tr) {
- insertOrUpdateFile(f, tr, insertFileStmt);
- }
-
- public void updateFile(DrawableFile f, DrawableTransaction tr) {
- insertOrUpdateFile(f, tr, updateFileStmt);
+ public void updateFile(DrawableFile f, DrawableTransaction tr, CaseDbTransaction caseDbTransaction) {
+ insertOrUpdateFile(f, tr, updateFileStmt, caseDbTransaction);
}
/**
@@ -603,7 +780,7 @@ public final class DrawableDB {
* @param tr a transaction to use, must not be null
* @param stmt the statement that does the actull inserting
*/
- private void insertOrUpdateFile(DrawableFile f, @Nonnull DrawableTransaction tr, @Nonnull PreparedStatement stmt) {
+ private void insertOrUpdateFile(DrawableFile f, @Nonnull DrawableTransaction tr, @Nonnull PreparedStatement stmt, @Nonnull CaseDbTransaction caseDbTransaction) {
if (tr.isClosed()) {
throw new IllegalArgumentException("can't update database with closed transaction");
@@ -611,15 +788,16 @@ public final class DrawableDB {
dbWriteLock();
try {
- // "INSERT OR IGNORE/ INTO drawable_files (path, name, created_time, modified_time, make, model, analyzed)"
+ // "INSERT OR IGNORE/ INTO drawable_files (obj_id, data_source_obj_id, path, name, created_time, modified_time, make, model, analyzed)"
stmt.setLong(1, f.getId());
- stmt.setString(2, f.getDrawablePath());
- stmt.setString(3, f.getName());
- stmt.setLong(4, f.getCrtime());
- stmt.setLong(5, f.getMtime());
- stmt.setString(6, f.getMake());
- stmt.setString(7, f.getModel());
- stmt.setBoolean(8, f.isAnalyzed());
+ stmt.setLong(2, f.getAbstractFile().getDataSource().getId());
+ stmt.setString(3, f.getDrawablePath());
+ stmt.setString(4, f.getName());
+ stmt.setLong(5, f.getCrtime());
+ stmt.setLong(6, f.getMtime());
+ stmt.setString(7, f.getMake());
+ stmt.setString(8, f.getModel());
+ stmt.setBoolean(9, f.isAnalyzed());
stmt.executeUpdate();
// Update the list of file IDs in memory
addImageFileToList(f.getId());
@@ -646,29 +824,32 @@ public final class DrawableDB {
}
}
} catch (TskCoreException ex) {
- LOGGER.log(Level.SEVERE, "failed to insert/update hash hits for file" + f.getContentPathSafe(), ex); //NON-NLS
+ logger.log(Level.SEVERE, "failed to insert/update hash hits for file" + f.getContentPathSafe(), ex); //NON-NLS
}
//and update all groups this file is in
for (DrawableAttribute> attr : DrawableAttribute.getGroupableAttrs()) {
Collection extends Comparable>> vals = attr.getValue(f);
for (Comparable> val : vals) {
- //use empty string for null values (mime_type), this shouldn't happen!
if (null != val) {
- insertGroup(val.toString(), attr);
+ if (attr == DrawableAttribute.PATH) {
+ insertGroup(f.getAbstractFile().getDataSource().getId(), val.toString(), attr, caseDbTransaction);
+ } else {
+ insertGroup(val.toString(), attr, caseDbTransaction);
+ }
}
}
}
tr.addUpdatedFile(f.getId());
- } catch (SQLException | NullPointerException ex) {
+ } catch (SQLException | NullPointerException | TskCoreException ex) {
/*
* This is one of the places where we get an error if the case is
* closed during processing, which doesn't need to be reported here.
*/
if (Case.isCaseOpen()) {
- LOGGER.log(Level.SEVERE, "failed to insert/update file" + f.getContentPathSafe(), ex); //NON-NLS
+ logger.log(Level.SEVERE, "failed to insert/update file" + f.getContentPathSafe(), ex); //NON-NLS
}
} finally {
@@ -676,6 +857,71 @@ public final class DrawableDB {
}
}
+ /**
+ * Gets all data source object ids from datasources table, and their
+ * DrawableDbBuildStatusEnum
+ *
+ * @return map of known data source object ids, and their db status
+ *
+ * @throws org.sleuthkit.datamodel.TskCoreException
+ */
+ public Map getDataSourceDbBuildStatus() throws TskCoreException {
+ Statement statement = null;
+ ResultSet rs = null;
+ Map map = new HashMap<>();
+ dbReadLock();
+ try {
+ statement = con.createStatement();
+ rs = statement.executeQuery("SELECT ds_obj_id, drawable_db_build_status FROM datasources "); //NON-NLS
+ while (rs.next()) {
+ map.put(rs.getLong("ds_obj_id"), DrawableDbBuildStatusEnum.valueOf(rs.getString("drawable_db_build_status")));
+ }
+ } catch (SQLException e) {
+ throw new TskCoreException("SQLException while getting data source object ids", e);
+ } finally {
+ if (rs != null) {
+ try {
+ rs.close();
+ } catch (SQLException ex) {
+ logger.log(Level.SEVERE, "Error closing resultset", ex); //NON-NLS
+ }
+ }
+ if (statement != null) {
+ try {
+ statement.close();
+ } catch (SQLException ex) {
+ logger.log(Level.SEVERE, "Error closing statement ", ex); //NON-NLS
+ }
+ }
+ dbReadUnlock();
+ }
+ return map;
+ }
+
+ /**
+ * Insert/update given data source object id and it's DB rebuild status in
+ * the datasources table.
+ *
+ * If the object id exists in the table already, it updates the status
+ *
+ * @param dsObjectId data source object id to insert
+ * @param status The db build statsus for datasource.
+ */
+ public void insertOrUpdateDataSource(long dsObjectId, DrawableDbBuildStatusEnum status) {
+ dbWriteLock();
+ try {
+ // "INSERT OR REPLACE INTO datasources (ds_obj_id, drawable_db_build_status) " //NON-NLS
+ updateDataSourceStmt.setLong(1, dsObjectId);
+ updateDataSourceStmt.setString(2, status.name());
+
+ updateDataSourceStmt.executeUpdate();
+ } catch (SQLException | NullPointerException ex) {
+ logger.log(Level.SEVERE, "failed to insert/update datasources table", ex); //NON-NLS
+ } finally {
+ dbWriteUnlock();
+ }
+ }
+
public DrawableTransaction beginTransaction() {
return new DrawableTransaction();
}
@@ -687,6 +933,13 @@ public final class DrawableDB {
tr.commit(notify);
}
+ public void rollbackTransaction(DrawableTransaction tr) {
+ if (tr.isClosed()) {
+ throw new IllegalArgumentException("can't rollback already closed transaction");
+ }
+ tr.rollback();
+ }
+
public Boolean isFileAnalyzed(DrawableFile f) {
return isFileAnalyzed(f.getId());
}
@@ -700,7 +953,7 @@ public final class DrawableDB {
}
} catch (SQLException ex) {
String msg = String.format("Failed to determine if file %s is finalized", String.valueOf(fileId)); //NON-NLS
- LOGGER.log(Level.WARNING, msg, ex);
+ logger.log(Level.WARNING, msg, ex);
} finally {
dbReadUnlock();
}
@@ -718,7 +971,7 @@ public final class DrawableDB {
return analyzedQuery.getInt(ANALYZED) == fileIds.size();
}
} catch (SQLException ex) {
- LOGGER.log(Level.WARNING, "problem counting analyzed files: ", ex); //NON-NLS
+ logger.log(Level.WARNING, "problem counting analyzed files: ", ex); //NON-NLS
} finally {
dbReadUnlock();
}
@@ -730,7 +983,6 @@ public final class DrawableDB {
dbReadLock();
try {
Set fileIDsInGroup = getFileIDsInGroup(gk);
-
try {
// In testing, this method appears to be a lot faster than doing one large select statement
for (Long fileID : fileIDsInGroup) {
@@ -745,10 +997,10 @@ public final class DrawableDB {
}
} catch (SQLException ex) {
- LOGGER.log(Level.WARNING, "problem counting analyzed files: ", ex); //NON-NLS
+ logger.log(Level.WARNING, "problem counting analyzed files: ", ex); //NON-NLS
}
} catch (TskCoreException tskCoreException) {
- LOGGER.log(Level.WARNING, "problem counting analyzed files: ", tskCoreException); //NON-NLS
+ logger.log(Level.WARNING, "problem counting analyzed files: ", tskCoreException); //NON-NLS
} finally {
dbReadUnlock();
}
@@ -768,33 +1020,18 @@ public final class DrawableDB {
* @throws TskCoreException
*/
public Set findAllFileIdsWhere(String sqlWhereClause) throws TskCoreException {
- Statement statement = null;
- ResultSet rs = null;
+
Set ret = new HashSet<>();
dbReadLock();
- try {
- statement = con.createStatement();
- rs = statement.executeQuery("SELECT obj_id FROM drawable_files WHERE " + sqlWhereClause); //NON-NLS
+ try (Statement statement = con.createStatement();
+ ResultSet rs = statement.executeQuery("SELECT obj_id FROM drawable_files WHERE " + sqlWhereClause);) {
while (rs.next()) {
ret.add(rs.getLong(1));
}
} catch (SQLException e) {
throw new TskCoreException("SQLException thrown when calling 'DrawableDB.findAllFileIdsWhere(): " + sqlWhereClause, e);
} finally {
- if (rs != null) {
- try {
- rs.close();
- } catch (SQLException ex) {
- LOGGER.log(Level.SEVERE, "Error closing result set after executing findAllFileIdsWhere", ex); //NON-NLS
- }
- }
- if (statement != null) {
- try {
- statement.close();
- } catch (SQLException ex) {
- LOGGER.log(Level.SEVERE, "Error closing statement after executing findAllFileIdsWhere", ex); //NON-NLS
- }
- }
+
dbReadUnlock();
}
return ret;
@@ -812,47 +1049,36 @@ public final class DrawableDB {
* @throws TskCoreException
*/
public long countFilesWhere(String sqlWhereClause) throws TskCoreException {
- Statement statement = null;
- ResultSet rs = null;
dbReadLock();
- try {
- statement = con.createStatement();
- rs = statement.executeQuery("SELECT COUNT (*) FROM drawable_files WHERE " + sqlWhereClause); //NON-NLS
- return rs.getLong(1);
+ try (Statement statement = con.createStatement();
+ ResultSet rs = statement.executeQuery("SELECT COUNT(*) AS COUNT FROM drawable_files WHERE " + sqlWhereClause);) {
+ return rs.getLong("COUNT");
} catch (SQLException e) {
throw new TskCoreException("SQLException thrown when calling 'DrawableDB.countFilesWhere(): " + sqlWhereClause, e);
} finally {
- if (rs != null) {
- try {
- rs.close();
- } catch (SQLException ex) {
- LOGGER.log(Level.SEVERE, "Error closing result set after executing countFilesWhere", ex); //NON-NLS
- }
- }
- if (statement != null) {
- try {
- statement.close();
- } catch (SQLException ex) {
- LOGGER.log(Level.SEVERE, "Error closing statement after executing countFilesWhere", ex); //NON-NLS
- }
- }
dbReadUnlock();
}
}
/**
+ * Get all the values that are in db for the given attribute.
*
*
- *
- * @param groupBy
- * @param sortBy
- * @param sortOrder
+ * @param The type of values for the given attribute.
+ * @param groupBy The attribute to get the values for.
+ * @param sortBy The way to sort the results. Only GROUP_BY_VAL and
+ * FILE_COUNT are supported.
+ * @param sortOrder Sort ascending or descending.
+ * @param dataSource
*
* @return
+ *
+ * @throws org.sleuthkit.datamodel.TskCoreException
*/
- public > List findValuesForAttribute(DrawableAttribute groupBy, GroupSortBy sortBy, SortOrder sortOrder) {
+ @SuppressWarnings("unchecked")
+ public > Multimap findValuesForAttribute(DrawableAttribute groupBy, GroupSortBy sortBy, SortOrder sortOrder, DataSource dataSource) throws TskCoreException {
- List vals = new ArrayList<>();
+ Multimap values = HashMultimap.create();
switch (groupBy.attrName) {
case ANALYZED:
@@ -864,7 +1090,14 @@ public final class DrawableDB {
default:
dbReadLock();
//TODO: convert this to prepared statement
- StringBuilder query = new StringBuilder("SELECT " + groupBy.attrName.toString() + ", COUNT(*) FROM drawable_files GROUP BY " + groupBy.attrName.toString()); //NON-NLS
+
+ StringBuilder query = new StringBuilder("SELECT data_source_obj_id, " + groupBy.attrName.toString() + ", COUNT(*) FROM drawable_files "); //NON-NLS
+
+ if (dataSource != null) {
+ query.append(" WHERE data_source_obj_id = ").append(dataSource.getId());
+ }
+
+ query.append(" GROUP BY data_source_obj_id, ").append(groupBy.attrName.toString());
String orderByClause = "";
@@ -893,49 +1126,73 @@ public final class DrawableDB {
}
try (Statement stmt = con.createStatement();
- ResultSet valsResults = stmt.executeQuery(query.toString())) {
- while (valsResults.next()) {
+ ResultSet results = stmt.executeQuery(query.toString())) {
+ while (results.next()) {
/*
- * I don't like that we have to do this cast here, but
- * can't think of a better alternative at the momment
- * unless something has gone seriously wrong, we know
- * this should be of type A even if JAVA doesn't
+ * I don't like that we have to do this cast to A here,
+ * but can't think of a better alternative at the
+ * momment unless something has gone seriously wrong, we
+ * know this should be of type A even if JAVA doesn't
*/
- @SuppressWarnings("unchecked")
- A value = (A) valsResults.getObject(groupBy.attrName.toString());
- vals.add(value);
+ values.put(tskCase.getDataSource(results.getLong("data_source_obj_id")),
+ (A) results.getObject(groupBy.attrName.toString()));
}
} catch (SQLException ex) {
- LOGGER.log(Level.WARNING, "Unable to get values for attribute", ex); //NON-NLS
+ if (!(ex.getCause() instanceof java.lang.InterruptedException)) {
+
+ /* It seems like this originaly comes out of c3p0 when
+ * its thread is intereupted (cancelled because of
+ * regroup). It should be safe to just swallow this and
+ * move on.
+ *
+ * see
+ * https://sourceforge.net/p/c3p0/mailman/c3p0-users/thread/EBB32BB8-6487-43AF-B291-9464C9051869@mchange.com/
+ */
+ throw new TskCoreException("Unable to get values for attribute", ex); //NON-NLS
+ }
+ } catch (TskDataException ex) {
+ throw new TskCoreException("Unable to get values for attribute", ex); //NON-NLS
} finally {
dbReadUnlock();
}
}
- return vals;
+ return values;
}
/**
* Insert new group into DB
- * @param value Value of the group (unique to the type)
- * @param groupBy Type of the grouping (CATEGORY, MAKE, etc.)
+ *
+ * @param value Value of the group (unique to the type)
+ * @param groupBy Type of the grouping (CATEGORY, MAKE, etc.)
+ * @param caseDbTransaction transaction to use for CaseDB insert/updates
*/
- private void insertGroup(final String value, DrawableAttribute> groupBy) {
- dbWriteLock();
+ private void insertGroup(final String value, DrawableAttribute> groupBy, CaseDbTransaction caseDbTransaction) {
+ insertGroup(0, value, groupBy, caseDbTransaction);
+ }
+ /**
+ * Insert new group into DB
+ *
+ * @param ds_obj_id data source object id
+ * @param value Value of the group (unique to the type)
+ * @param groupBy Type of the grouping (CATEGORY, MAKE, etc.)
+ * @param caseDbTransaction transaction to use for CaseDB insert/updates
+ */
+ private void insertGroup(long ds_obj_id, final String value, DrawableAttribute> groupBy, CaseDbTransaction caseDbTransaction) {
try {
- //PreparedStatement insertGroup = con.prepareStatement("insert or replace into groups (value, attribute, seen) values (?,?,0)");
- insertGroupStmt.clearParameters();
- insertGroupStmt.setString(1, value);
- insertGroupStmt.setString(2, groupBy.attrName.toString());
- insertGroupStmt.execute();
- } catch (SQLException sQLException) {
+ String insertSQL = String.format(" (data_source_obj_id, value, attribute) VALUES (%d, \'%s\', \'%s\')",
+ ds_obj_id, value, groupBy.attrName.toString());
+
+ if (DbType.POSTGRESQL == tskCase.getDatabaseType()) {
+ insertSQL += "ON CONFLICT DO NOTHING";
+ }
+ tskCase.getCaseDbAccessManager().insert(GROUPS_TABLENAME, insertSQL, caseDbTransaction);
+ } catch (TskCoreException ex) {
// Don't need to report it if the case was closed
if (Case.isCaseOpen()) {
- LOGGER.log(Level.SEVERE, "Unable to insert group", sQLException); //NON-NLS
+ logger.log(Level.SEVERE, "Unable to insert group", ex); //NON-NLS
}
- } finally {
- dbWriteUnlock();
}
}
@@ -953,8 +1210,8 @@ public final class DrawableDB {
return DrawableFile.create(f,
areFilesAnalyzed(Collections.singleton(id)), isVideoFile(f));
} catch (IllegalStateException ex) {
- LOGGER.log(Level.SEVERE, "there is no case open; failed to load file with id: {0}", id); //NON-NLS
- return null;
+ logger.log(Level.SEVERE, "there is no case open; failed to load file with id: {0}", id); //NON-NLS
+ throw new TskCoreException("there is no case open; failed to load file with id: " + id, ex);
}
}
@@ -973,8 +1230,8 @@ public final class DrawableDB {
Set files = new HashSet<>();
dbReadLock();
try {
- PreparedStatement statement = getGroupStatment(groupKey.getAttribute());
- statement.setObject(1, groupKey.getValue());
+ PreparedStatement statement = getGroupStatment(groupKey);
+ setQueryParams(statement, groupKey);
try (ResultSet valsResults = statement.executeQuery()) {
while (valsResults.next()) {
@@ -982,7 +1239,7 @@ public final class DrawableDB {
}
}
} catch (SQLException ex) {
- LOGGER.log(Level.WARNING, "failed to get file for group:" + groupKey.getAttribute() + " == " + groupKey.getValue(), ex); //NON-NLS
+ logger.log(Level.WARNING, "failed to get file for group:" + groupKey.getAttribute() + " == " + groupKey.getValue(), ex); //NON-NLS
} finally {
dbReadUnlock();
}
@@ -996,26 +1253,26 @@ public final class DrawableDB {
}
}
- private PreparedStatement getGroupStatment(DrawableAttribute> groupBy) {
- return groupStatementMap.get(groupBy);
+ private PreparedStatement getGroupStatment(GroupKey> groupKey) {
+ DrawableAttribute> groupBy = groupKey.getAttribute();
+ if ((groupBy == DrawableAttribute.PATH) && groupKey.getDataSource().isPresent()) {
+ return this.groupStatementFilterByDataSrcMap.get(groupBy);
+ }
+
+ return groupStatementMap.get(groupBy);
}
- public int countAllFiles() {
- int result = -1;
- dbReadLock();
- try (ResultSet rs = con.createStatement().executeQuery("SELECT COUNT(*) AS COUNT FROM drawable_files")) { //NON-NLS
- while (rs.next()) {
+ public long countAllFiles() throws TskCoreException {
+ return countAllFiles(null);
+ }
- result = rs.getInt("COUNT");
- break;
- }
- } catch (SQLException ex) {
- LOGGER.log(Level.SEVERE, "Error accessing SQLite database"); //NON-NLS
- } finally {
- dbReadUnlock();
+ public long countAllFiles(DataSource dataSource) throws TskCoreException {
+ if (null != dataSource) {
+ return countFilesWhere(" data_source_obj_id = ");
+ } else {
+ return countFilesWhere(" 1 ");
}
- return result;
}
/**
@@ -1043,7 +1300,7 @@ public final class DrawableDB {
//TODO: delete from hash_set_hits table also...
} catch (SQLException ex) {
- LOGGER.log(Level.WARNING, "failed to delete row for obj_id = " + id, ex); //NON-NLS
+ logger.log(Level.WARNING, "failed to delete row for obj_id = " + id, ex); //NON-NLS
} finally {
dbWriteUnlock();
}
@@ -1055,10 +1312,8 @@ public final class DrawableDB {
public class MultipleTransactionException extends IllegalStateException {
- private static final String CANNOT_HAVE_MORE_THAN_ONE_OPEN_TRANSACTIO = "cannot have more than one open transaction"; //NON-NLS
-
public MultipleTransactionException() {
- super(CANNOT_HAVE_MORE_THAN_ONE_OPEN_TRANSACTIO);
+ super("cannot have more than one open transaction");//NON-NLS
}
}
@@ -1097,14 +1352,13 @@ public final class DrawableDB {
private void initializeImageList() {
synchronized (fileIDsInDB) {
dbReadLock();
- try {
- Statement stmt = con.createStatement();
- ResultSet analyzedQuery = stmt.executeQuery("select obj_id from drawable_files"); //NON-NLS
+ try (Statement stmt = con.createStatement();
+ ResultSet analyzedQuery = stmt.executeQuery("select obj_id from drawable_files");) {
while (analyzedQuery.next()) {
addImageFileToList(analyzedQuery.getLong(OBJ_ID));
}
} catch (SQLException ex) {
- LOGGER.log(Level.WARNING, "problem loading file IDs: ", ex); //NON-NLS
+ logger.log(Level.WARNING, "problem loading file IDs: ", ex); //NON-NLS
} finally {
dbReadUnlock();
}
@@ -1155,9 +1409,9 @@ public final class DrawableDB {
.count();
}
} 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
} catch (TskCoreException ex1) {
- LOGGER.log(Level.SEVERE, "Failed to get content tags by tag name.", ex1); //NON-NLS
+ logger.log(Level.SEVERE, "Failed to get content tags by tag name.", ex1); //NON-NLS
}
return -1;
@@ -1176,20 +1430,20 @@ public final class DrawableDB {
*
* @param fileIDs the the files ids to count within
*
- * @return the number of files with Cat-0
+ * @return the number of files in the given set with Cat-0
*/
- public long getUncategorizedCount(Collection fileIDs) {
-
+ public long getUncategorizedCount(Collection fileIDs) throws TskCoreException {
+
// if the fileset is empty, return count as 0
if (fileIDs.isEmpty()) {
return 0;
}
-
- DrawableTagsManager tagsManager = controller.getTagsManager();
// get a comma seperated list of TagName ids for non zero categories
- String catTagNameIDs = DhsImageCategory.getNonZeroCategories().stream()
- .map(tagsManager::getTagName)
+ DrawableTagsManager tagsManager = controller.getTagsManager();
+
+ String catTagNameIDs = tagsManager.getCategoryTagNames().stream()
+ .filter(tagName -> notEqual(tagName.getDisplayName(), DhsImageCategory.ZERO.getDisplayName()))
.map(TagName::getId)
.map(Object::toString)
.collect(Collectors.joining(",", "(", ")"));
@@ -1197,17 +1451,18 @@ public final class DrawableDB {
String fileIdsList = "(" + StringUtils.join(fileIDs, ",") + " )";
//count the file ids that are in the given list and don't have a non-zero category assigned to them.
- String name =
- "SELECT COUNT(obj_id) as obj_count FROM tsk_files where obj_id IN " + fileIdsList //NON-NLS
- + " AND obj_id NOT IN (SELECT obj_id FROM content_tags WHERE content_tags.tag_name_id IN " + catTagNameIDs + ")"; //NON-NLS
+ String name
+ = "SELECT COUNT(obj_id) as obj_count FROM tsk_files where obj_id IN " + fileIdsList //NON-NLS
+ + " AND obj_id NOT IN (SELECT obj_id FROM content_tags WHERE content_tags.tag_name_id IN " + catTagNameIDs + ")"; //NON-NLS
try (SleuthkitCase.CaseDbQuery executeQuery = tskCase.executeQuery(name);
ResultSet resultSet = executeQuery.getResultSet();) {
while (resultSet.next()) {
return resultSet.getLong("obj_count"); //NON-NLS
}
- } catch (SQLException | TskCoreException ex) {
- LOGGER.log(Level.SEVERE, "Error getting category count.", ex); //NON-NLS
+ } catch (SQLException ex) {
+ throw new TskCoreException("Error getting category count.", ex); //NON-NLS
}
+
return -1;
}
@@ -1240,7 +1495,7 @@ public final class DrawableDB {
con.setAutoCommit(false);
} catch (SQLException ex) {
- LOGGER.log(Level.SEVERE, "failed to set auto-commit to to false", ex); //NON-NLS
+ logger.log(Level.SEVERE, "failed to set auto-commit to to false", ex); //NON-NLS
}
}
@@ -1251,7 +1506,7 @@ public final class DrawableDB {
con.rollback();
updatedFiles.clear();
} catch (SQLException ex1) {
- LOGGER.log(Level.SEVERE, "Exception while attempting to rollback!!", ex1); //NON-NLS
+ logger.log(Level.SEVERE, "Exception while attempting to rollback!!", ex1); //NON-NLS
} finally {
close();
}
@@ -1273,9 +1528,9 @@ public final class DrawableDB {
}
} catch (SQLException ex) {
if (Case.isCaseOpen()) {
- LOGGER.log(Level.SEVERE, "Error commiting drawable.db.", ex); //NON-NLS
+ logger.log(Level.SEVERE, "Error commiting drawable.db.", ex); //NON-NLS
} else {
- LOGGER.log(Level.WARNING, "Error commiting drawable.db - case is closed."); //NON-NLS
+ logger.log(Level.WARNING, "Error commiting drawable.db - case is closed."); //NON-NLS
}
rollback();
}
@@ -1288,9 +1543,9 @@ public final class DrawableDB {
con.setAutoCommit(true);
} catch (SQLException ex) {
if (Case.isCaseOpen()) {
- LOGGER.log(Level.SEVERE, "Error setting auto-commit to true.", ex); //NON-NLS
+ logger.log(Level.SEVERE, "Error setting auto-commit to true.", ex); //NON-NLS
} else {
- LOGGER.log(Level.SEVERE, "Error setting auto-commit to true - case is closed"); //NON-NLS
+ logger.log(Level.SEVERE, "Error setting auto-commit to true - case is closed"); //NON-NLS
}
} finally {
closed = true;
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java
index 34c9d5a4c5..e22151f934 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java
@@ -18,7 +18,6 @@
*/
package org.sleuthkit.autopsy.imagegallery.datamodel;
-import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import java.lang.ref.SoftReference;
import java.text.MessageFormat;
import java.util.ArrayList;
@@ -32,6 +31,7 @@ import java.util.stream.Collectors;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Task;
+import javafx.concurrent.Worker;
import javafx.scene.image.Image;
import javafx.util.Pair;
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.NoCurrentCaseException;
import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.imagegallery.FileTypeUtils;
-import org.sleuthkit.autopsy.imagegallery.ThumbnailCache;
import org.sleuthkit.autopsy.imagegallery.utils.TaskUtils;
import org.sleuthkit.datamodel.AbstractFile;
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.
+ *
+ * @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
- ? new VideoFile(abstractFileById, analyzed)
- : new ImageFile(abstractFileById, analyzed);
+ ? new VideoFile(file, analyzed)
+ : new ImageFile(file, analyzed);
}
- public static DrawableFile create(Long id, boolean analyzed) throws TskCoreException, NoCurrentCaseException {
- return create(Case.getCurrentCaseThrows().getSleuthkitCase().getAbstractFileById(id), analyzed);
+ public static DrawableFile create(Long fileID, boolean analyzed) throws TskCoreException, NoCurrentCaseException {
+ return create(Case.getCurrentCaseThrows().getSleuthkitCase().getAbstractFileById(fileID), analyzed);
}
private SoftReference imageRef;
@@ -149,8 +155,8 @@ public abstract class DrawableFile {
return file.getSleuthkitCase();
}
- private Pair, Collection>> makeAttributeValuePair(DrawableAttribute> t) {
- return new Pair<>(t, t.getValue(DrawableFile.this));
+ private Pair, Collection>> makeAttributeValuePair(DrawableAttribute> attribute) {
+ return new Pair<>(attribute, attribute.getValue(this));
}
public String getModel() {
@@ -254,42 +260,17 @@ public abstract class DrawableFile {
return getSleuthkitCase().getContentTagsByContent(file);
}
- @Deprecated
- public Image getThumbnail() {
- try {
- return getThumbnailTask().get();
- } catch (InterruptedException | ExecutionException ex) {
- return null;
- }
-
- }
-
- public Task 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 getReadFullSizeImageTask() {
Image image = (imageRef != null) ? imageRef.get() : null;
if (image == null || image.isError()) {
Task readImageTask = getReadFullSizeImageTaskHelper();
readImageTask.stateProperty().addListener(stateProperty -> {
- switch (readImageTask.getState()) {
- case SUCCEEDED:
- try {
- imageRef = new SoftReference<>(readImageTask.get());
- } catch (InterruptedException | ExecutionException exception) {
- LOGGER.log(Level.WARNING, getMessageTemplate(exception), getContentPathSafe());
- }
- break;
+ if (readImageTask.getState() == Worker.State.SUCCEEDED) {
+ try {
+ imageRef = new SoftReference<>(readImageTask.get());
+ } catch (InterruptedException | ExecutionException exception) {
+ LOGGER.log(Level.WARNING, getMessageTemplate(exception), getContentPathSafe());
+ }
}
});
return readImageTask;
@@ -316,14 +297,14 @@ public abstract class DrawableFile {
/**
* Get the width of the visual content.
- *
+ *
* @return The width.
*/
abstract Double getWidth();
/**
* Get the height of the visual content.
- *
+ *
* @return The height.
*/
abstract Double getHeight();
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableTagsManager.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableTagsManager.java
index cb30c37bab..90b7b5c398 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableTagsManager.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableTagsManager.java
@@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
- * Copyright 2013-16 Basis Technology Corp.
+ * Copyright 2013-18 Basis Technology Corp.
* Contact: carrier sleuthkit org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,25 +18,23 @@
*/
package org.sleuthkit.autopsy.imagegallery.datamodel;
-import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import com.google.common.eventbus.AsyncEventBus;
import com.google.common.eventbus.EventBus;
-import java.util.Collections;
import java.util.List;
-import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
-import javax.annotation.Nonnull;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent;
import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent;
import org.sleuthkit.autopsy.casemodule.services.TagsManager;
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.ContentTag;
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
- * 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",
"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 Image BOOKMARK_IMAGE;
+ private static final Image FOLLOW_UP_IMAGE = new Image("/org/sleuthkit/autopsy/imagegallery/images/flag_red.png");
+ private static final Image BOOKMARK_IMAGE = new Image("/org/sleuthkit/autopsy/images/star-bookmark-icon-16.png");
- final private Object autopsyTagsManagerLock = new Object();
- private TagsManager autopsyTagsManager;
+ private final 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(
- Executors.newSingleThreadExecutor(
- new BasicThreadFactory.Builder().namingPattern("Tags Event Bus").uncaughtExceptionHandler((Thread t, Throwable e) -> { //NON-NLS
- LOGGER.log(Level.SEVERE, "uncaught exception in event bus handler", e); //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;
+ private final EventBus tagsEventBus
+ = new AsyncEventBus(
+ Executors.newSingleThreadExecutor(
+ new BasicThreadFactory.Builder()
+ .namingPattern("Tags Event Bus")//NON-NLS
+ .uncaughtExceptionHandler((Thread thread, Throwable throwable)
+ -> logger.log(Level.SEVERE, "Uncaught exception in DrawableTagsManager event bus handler.", throwable)) //NON-NLS
+ .build()));
+ 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
- * changes
+ * Get the follow up TagName.
*
- * @param autopsyTagsManager
+ * @return The follow up TagName.
*/
- public void setAutopsyTagsManager(TagsManager autopsyTagsManager) {
- synchronized (autopsyTagsManagerLock) {
- this.autopsyTagsManager = autopsyTagsManager;
- clearFollowUpTagName();
- }
+ public TagName getFollowUpTagName() {
+ return followUpTagName;
}
/**
- * Use when closing a case to make sure everything is re-initialized in the
- * next case.
+ * Get the bookmark TagName.
+ *
+ * @return The bookmark TagName.
*/
- public void clearFollowUpTagName() {
- synchronized (autopsyTagsManagerLock) {
- followUpTagName = null;
- }
+ private TagName getBookmarkTagName() throws TskCoreException {
+ return bookmarkTagName;
}
/**
- * get the (cached) follow up TagName
+ * Get all the TagNames that are not categories
*
- * @return
+ * @return All the TagNames that are not categories, in alphabetical order
+ * by displayName.
*
- * @throws TskCoreException
+ * @throws org.sleuthkit.datamodel.TskCoreException
*/
- public TagName getFollowUpTagName() throws TskCoreException {
- synchronized (autopsyTagsManagerLock) {
- if (Objects.isNull(followUpTagName)) {
- followUpTagName = getTagName(NbBundle.getMessage(DrawableTagsManager.class, "DrawableTagsManager.followUp"));
- }
- return followUpTagName;
- }
+ public List getNonCategoryTagNames() throws TskCoreException {
+ return autopsyTagsManager.getAllTagNames().stream()
+ .filter(CategoryManager::isNotCategoryTagName)
+ .distinct().sorted()
+ .collect(Collectors.toList());
}
- 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 all the TagNames that are categories
*
- * @return all the TagNames that are not categories, in alphabetical order
- * by displayName, or, an empty set if there was an exception
- * looking them up from the db.
+ * @return All the TagNames that are categories, in alphabetical order by
+ * displayName.
+ *
+ * @throws org.sleuthkit.datamodel.TskCoreException
*/
- @Nonnull
- public List getNonCategoryTagNames() {
- synchronized (autopsyTagsManagerLock) {
- try {
- return autopsyTagsManager.getAllTagNames().stream()
- .filter(CategoryManager::isNotCategoryTagName)
- .distinct().sorted()
- .collect(Collectors.toList());
- } catch (TskCoreException | IllegalStateException ex) {
- LOGGER.log(Level.WARNING, "couldn't access case", ex); //NON-NLS
- }
- return Collections.emptyList();
- }
+ public List getCategoryTagNames() throws TskCoreException {
+ return autopsyTagsManager.getAllTagNames().stream()
+ .filter(CategoryManager::isCategoryTagName)
+ .distinct().sorted()
+ .collect(Collectors.toList());
}
/**
@@ -187,9 +161,7 @@ public class DrawableTagsManager {
* @throws TskCoreException if there was an error reading from the db
*/
public List getContentTags(Content content) throws TskCoreException {
- synchronized (autopsyTagsManagerLock) {
- return autopsyTagsManager.getContentTagsByContent(content);
- }
+ return autopsyTagsManager.getContentTagsByContent(content);
}
/**
@@ -207,91 +179,56 @@ public class DrawableTagsManager {
}
public TagName getTagName(String displayName) throws TskCoreException {
- synchronized (autopsyTagsManagerLock) {
- try {
- TagName returnTagName = autopsyTagsManager.getDisplayNamesToTagNamesMap().get(displayName);
- if (returnTagName != null) {
- return returnTagName;
- }
- try {
- return autopsyTagsManager.addTagName(displayName);
- } catch (TagsManager.TagNameAlreadyExistsException ex) {
- returnTagName = autopsyTagsManager.getDisplayNamesToTagNamesMap().get(displayName);
- if (returnTagName != null) {
- return returnTagName;
- }
- 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);
+
+ TagName returnTagName = autopsyTagsManager.getDisplayNamesToTagNamesMap().get(displayName);
+ if (returnTagName != null) {
+ return returnTagName;
+ }
+ try {
+ return autopsyTagsManager.addTagName(displayName);
+ } catch (TagsManager.TagNameAlreadyExistsException ex) {
+ returnTagName = autopsyTagsManager.getDisplayNamesToTagNamesMap().get(displayName);
+ if (returnTagName != null) {
+ return returnTagName;
}
+ throw new TskCoreException("Tag name exists but an error occured in retrieving it", ex);
}
}
- public TagName getTagName(DhsImageCategory cat) {
- try {
- return getTagName(cat.getDisplayName());
- } catch (TskCoreException ex) {
- return null;
- }
+ public TagName getTagName(DhsImageCategory cat) throws TskCoreException {
+ return getTagName(cat.getDisplayName());
}
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 getContentTagsByTagName(TagName t) throws TskCoreException {
- synchronized (autopsyTagsManagerLock) {
- return autopsyTagsManager.getContentTagsByTagName(t);
- }
+ public List getContentTagsByTagName(TagName tagName) throws TskCoreException {
+ return autopsyTagsManager.getContentTagsByTagName(tagName);
}
public List getAllTagNames() throws TskCoreException {
- synchronized (autopsyTagsManagerLock) {
- return autopsyTagsManager.getAllTagNames();
- }
+ return autopsyTagsManager.getAllTagNames();
}
public List getTagNamesInUse() throws TskCoreException {
- synchronized (autopsyTagsManagerLock) {
- return autopsyTagsManager.getTagNamesInUse();
- }
+ return autopsyTagsManager.getTagNamesInUse();
}
- public void deleteContentTag(ContentTag ct) throws TskCoreException {
- synchronized (autopsyTagsManagerLock) {
- autopsyTagsManager.deleteContentTag(ct);
- }
+ public void deleteContentTag(ContentTag contentTag) throws TskCoreException {
+ autopsyTagsManager.deleteContentTag(contentTag);
}
public Node getGraphic(TagName tagname) {
try {
if (tagname.equals(getFollowUpTagName())) {
- return new ImageView(getFollowUpImage());
+ return new ImageView(FOLLOW_UP_IMAGE);
} else if (tagname.equals(getBookmarkTagName())) {
- return new ImageView(getBookmarkImage());
+ return new ImageView(BOOKMARK_IMAGE);
}
} 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);
}
-
- 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;
- }
-
}
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/HashSetManager.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/HashSetManager.java
index 9b3b3218f1..19eab6792a 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/HashSetManager.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/HashSetManager.java
@@ -1,8 +1,26 @@
-package org.sleuthkit.autopsy.imagegallery.datamodel;
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013-18 Basis Technology Corp.
+ * Contact: carrier sleuthkit 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.CacheLoader;
import com.google.common.cache.LoadingCache;
+import java.sql.SQLException;
import java.util.Collections;
import java.util.Set;
import java.util.logging.Level;
@@ -15,26 +33,18 @@ import org.sleuthkit.datamodel.TskCoreException;
*/
public class HashSetManager {
- /**
- * The db that initial values are loaded from.
- */
- private DrawableDB db = null;
+ /** The db that initial values are loaded from. */
+ private final DrawableDB drawableDB;
+
+ public HashSetManager(DrawableDB drawableDB) {
+ this.drawableDB = drawableDB;
+ }
/**
* the internal cache from fileID to a set of hashset names.
*/
private final LoadingCache> 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
*
@@ -44,9 +54,14 @@ public class HashSetManager {
*/
private Set getHashSetsForFileHelper(long fileID) {
try {
- return db.getHashSetsForFile(fileID);
- } catch (TskCoreException ex) {
- Logger.getLogger(HashSetManager.class.getName()).log(Level.SEVERE, "Failed to get Hash Sets for file", ex); //NON-NLS
+ if (drawableDB.isClosed()) {
+ Logger.getLogger(HashSetManager.class.getName()).log(Level.WARNING, "Failed to get Hash Sets for file. The Db connection was already closed."); //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();
}
}
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/DrawableGroup.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/DrawableGroup.java
index 3ef949c9c9..224ac378c6 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/DrawableGroup.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/DrawableGroup.java
@@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
- * Copyright 2013-16 Basis Technology Corp.
+ * Copyright 2013-18 Basis Technology Corp.
* Contact: carrier sleuthkit org
*
* 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.DoubleBinding;
import javafx.beans.binding.IntegerBinding;
+import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.ReadOnlyLongProperty;
import javafx.beans.property.ReadOnlyLongWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
+import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
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.DrawableAttribute;
+import org.sleuthkit.datamodel.TskCoreException;
/**
* Represents a set of image/video files in a group. The UI listens to changes
@@ -76,7 +79,7 @@ public class DrawableGroup implements Comparable {
}
@SuppressWarnings("ReturnOfCollectionOrArrayField")
- public synchronized ObservableList getFileIDs() {
+ public ObservableList getFileIDs() {
return unmodifiableFileIDS;
}
@@ -121,11 +124,11 @@ public class DrawableGroup implements Comparable {
if (hashSetHitsCount.get() < 0) {
try {
hashSetHitsCount.set(fileIDs.stream()
- .map(fileID -> ImageGalleryController.getDefault().getHashSetManager().isInAnyHashSet(fileID))
+ .map(ImageGalleryModule.getController().getHashSetManager()::isInAnyHashSet)
.filter(Boolean::booleanValue)
.count());
- } catch (IllegalStateException | NullPointerException ex) {
- LOGGER.log(Level.WARNING, "could not access case during getFilesWithHashSetHitsCount()"); //NON-NLS
+ } catch (NoCurrentCaseException ex) {
+ LOGGER.log(Level.WARNING, "Could not access case during getFilesWithHashSetHitsCount()"); //NON-NLS
}
}
return hashSetHitsCount.get();
@@ -139,10 +142,10 @@ public class DrawableGroup implements Comparable {
public final synchronized long getUncategorizedCount() {
if (uncatCount.get() < 0) {
try {
- uncatCount.set(ImageGalleryController.getDefault().getDatabase().getUncategorizedCount(fileIDs));
+ uncatCount.set(ImageGalleryModule.getController().getDatabase().getUncategorizedCount(fileIDs));
- } catch (IllegalStateException | NullPointerException ex) {
- LOGGER.log(Level.WARNING, "could not access case during getFilesWithHashSetHitsCount()"); //NON-NLS
+ } catch (TskCoreException | NoCurrentCaseException ex) {
+ LOGGER.log(Level.WARNING, "Could not access case during getFilesWithHashSetHitsCount()"); //NON-NLS
}
}
@@ -163,8 +166,8 @@ public class DrawableGroup implements Comparable {
return seen.get();
}
- public ReadOnlyBooleanWrapper seenProperty() {
- return seen;
+ public ReadOnlyBooleanProperty seenProperty() {
+ return seen.getReadOnlyProperty();
}
@Subscribe
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupKey.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupKey.java
index 9945a72313..8b9cb845d1 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupKey.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupKey.java
@@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
- * Copyright 2013-16 Basis Technology Corp.
+ * Copyright 2013-18 Basis Technology Corp.
* Contact: carrier sleuthkit org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,27 +18,31 @@
*/
package org.sleuthkit.autopsy.imagegallery.datamodel.grouping;
-import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
import javafx.scene.Node;
import javax.annotation.concurrent.Immutable;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute;
+import org.sleuthkit.datamodel.DataSource;
import org.sleuthkit.datamodel.TagName;
/**
- * key identifying information of a {@link Grouping}. Used to look up groups in
- * {@link Map}s and from the db.
+ * Key identifying information of a DrawableGroup. Used to look up groups in
+ * Maps and from the db.
+ *
+ * @param The type of the values of the attribute this key uses.
*/
@Immutable
public class GroupKey> implements Comparable> {
private final T val;
-
private final DrawableAttribute attr;
+ private final DataSource dataSource;
- public GroupKey(DrawableAttribute attr, T val) {
+ public GroupKey(DrawableAttribute attr, T val, DataSource dataSource) {
this.attr = attr;
this.val = val;
+ this.dataSource = dataSource;
}
public T getValue() {
@@ -49,6 +53,10 @@ public class GroupKey> implements Comparable
return attr;
}
+ public Optional< DataSource> getDataSource() {
+ return Optional.ofNullable(dataSource);
+ }
+
public String getValueDisplayName() {
return Objects.equals(attr, DrawableAttribute.TAGS)
? ((TagName) getValue()).getDisplayName()
@@ -63,13 +71,19 @@ public class GroupKey> implements Comparable
@Override
public int hashCode() {
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;
}
@Override
public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
if (obj == null) {
return false;
}
@@ -77,11 +91,14 @@ public class GroupKey> implements Comparable
return false;
}
final GroupKey> other = (GroupKey>) obj;
- if (this.attr != other.attr) {
+ if (!Objects.equals(this.val, other.val)) {
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
@@ -92,4 +109,8 @@ public class GroupKey> implements Comparable
public Node getGraphic() {
return attr.getGraphicForValue(val);
}
+
+ public long getDataSourceObjId() {
+ return getDataSource().map(DataSource::getId).orElse(0L);
+ }
}
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java
index fb381160f7..95d00ee84a 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java
@@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
- * Copyright 2013-16 Basis Technology Corp.
+ * Copyright 2013-18 Basis Technology Corp.
* Contact: carrier sleuthkit org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,10 +18,14 @@
*/
package org.sleuthkit.autopsy.imagegallery.datamodel.grouping;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
import com.google.common.eventbus.Subscribe;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
import java.sql.ResultSet;
import java.sql.SQLException;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@@ -31,33 +35,32 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
-import static java.util.Objects.nonNull;
+import static java.util.Objects.isNull;
+import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
-import java.util.concurrent.ExecutorService;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
-import java.util.stream.Stream;
import javafx.application.Platform;
+import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.ReadOnlyDoubleProperty;
-import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
-import static javafx.concurrent.Worker.State.CANCELLED;
-import static javafx.concurrent.Worker.State.FAILED;
-import static javafx.concurrent.Worker.State.READY;
-import static javafx.concurrent.Worker.State.RUNNING;
-import static javafx.concurrent.Worker.State.SCHEDULED;
-import static javafx.concurrent.Worker.State.SUCCEEDED;
+import javafx.concurrent.Service;
+import javafx.concurrent.Task;
+import javafx.concurrent.Worker;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.swing.SortOrder;
-import org.apache.commons.lang3.ObjectUtils;
+import static org.apache.commons.collections4.CollectionUtils.isNotEmpty;
+import static org.apache.commons.lang3.ObjectUtils.notEqual;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.netbeans.api.progress.ProgressHandle;
@@ -68,82 +71,71 @@ import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent;
import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent;
import org.sleuthkit.autopsy.coreutils.LoggedTask;
import org.sleuthkit.autopsy.coreutils.Logger;
-import org.sleuthkit.autopsy.coreutils.ThreadConfined;
-import org.sleuthkit.autopsy.coreutils.ThreadConfined.ThreadType;
-import org.sleuthkit.autopsy.imagegallery.ImageGalleryController;
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.DrawableAttribute;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableDB;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableTagsManager;
import org.sleuthkit.datamodel.AbstractFile;
+import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.ContentTag;
+import org.sleuthkit.datamodel.DataSource;
+import org.sleuthkit.datamodel.Examiner;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.TagName;
import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.datamodel.TskData.DbType;
/**
- * Provides an abstraction layer on top of {@link DrawableDB} ( and to some
- * extent {@link SleuthkitCase} ) to facilitate creation, retrieval, updating,
- * and sorting of {@link DrawableGroup}s.
+ * Provides an abstraction layer on top of DrawableDB ( and to some extent
+ * SleuthkitCase ) to facilitate creation, retrieval, updating, and sorting of
+ * DrawableGroups.
*/
public class GroupManager {
- private static final Logger LOGGER = Logger.getLogger(GroupManager.class.getName());
+ private static final Logger logger = Logger.getLogger(GroupManager.class.getName());
- private DrawableDB db;
+ /** An executor to submit async UI related background tasks to. */
+ private final ListeningExecutorService exec = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(
+ new BasicThreadFactory.Builder().namingPattern("GroupManager BG Thread-%d").build())); //NON-NLS
private final ImageGalleryController controller;
- /**
- * map from {@link GroupKey}s to {@link DrawableGroup}s. All groups (even
- * not fully analyzed or not visible groups could be in this map
- */
- @GuardedBy("this")
- private final Map, DrawableGroup> groupMap = new HashMap<>();
-
- /**
- * list of all analyzed groups
- */
- @ThreadConfined(type = ThreadType.JFX)
+ /** list of all analyzed groups */
+ @GuardedBy("this") //NOPMD
private final ObservableList analyzedGroups = FXCollections.observableArrayList();
private final ObservableList unmodifiableAnalyzedGroups = FXCollections.unmodifiableObservableList(analyzedGroups);
- /**
- * list of unseen groups
- */
- @ThreadConfined(type = ThreadType.JFX)
+ /** list of unseen groups */
+ @GuardedBy("this") //NOPMD
private final ObservableList unSeenGroups = FXCollections.observableArrayList();
private final ObservableList unmodifiableUnSeenGroups = FXCollections.unmodifiableObservableList(unSeenGroups);
-
- private ReGroupTask> groupByTask;
+ /**
+ * map from GroupKey} to DrawableGroupSs. All groups (even not fully
+ * analyzed or not visible groups could be in this map
+ */
+ @GuardedBy("this") //NOPMD
+ private final Map, DrawableGroup> groupMap = new HashMap<>();
/*
* --- current grouping/sorting attributes ---
*/
- private volatile GroupSortBy sortBy = GroupSortBy.PRIORITY;
- private volatile DrawableAttribute> groupBy = DrawableAttribute.PATH;
- private volatile SortOrder sortOrder = SortOrder.ASCENDING;
+ @GuardedBy("this") //NOPMD
+ private final ReadOnlyObjectWrapper< GroupSortBy> sortByProp = new ReadOnlyObjectWrapper<>(GroupSortBy.PRIORITY);
+ private final ReadOnlyObjectWrapper< DrawableAttribute>> groupByProp = new ReadOnlyObjectWrapper<>(DrawableAttribute.PATH);
+ private final ReadOnlyObjectWrapper sortOrderProp = new ReadOnlyObjectWrapper<>(SortOrder.ASCENDING);
+ private final ReadOnlyObjectWrapper dataSourceProp = new ReadOnlyObjectWrapper<>(null);//null indicates all datasources
+ private final ReadOnlyBooleanWrapper collaborativeModeProp = new ReadOnlyBooleanWrapper(false);
- private final ReadOnlyObjectWrapper< Comparator> sortByProp = new ReadOnlyObjectWrapper<>(sortBy);
- private final ReadOnlyObjectWrapper< DrawableAttribute>> groupByProp = new ReadOnlyObjectWrapper<>(groupBy);
- private final ReadOnlyObjectWrapper sortOrderProp = new ReadOnlyObjectWrapper<>(sortOrder);
-
- private final ReadOnlyDoubleWrapper regroupProgress = new ReadOnlyDoubleWrapper();
-
- public void setDB(DrawableDB db) {
- this.db = db;
- regroup(groupBy, sortBy, sortOrder, Boolean.TRUE);
- }
+ private final GroupingService regrouper;
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public ObservableList getAnalyzedGroups() {
return unmodifiableAnalyzedGroups;
}
- @ThreadConfined(type = ThreadType.JFX)
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public ObservableList getUnSeenGroups() {
return unmodifiableUnSeenGroups;
@@ -152,52 +144,52 @@ public class GroupManager {
/**
* construct a group manager hooked up to the given db and controller
*
- * @param db
* @param controller
*/
public GroupManager(ImageGalleryController controller) {
this.controller = controller;
+ this.regrouper = new GroupingService();
+ regrouper.setExecutor(exec);
}
/**
- * using the current groupBy set for this manager, find groupkeys for all
+ * Using the current groupBy set for this manager, find groupkeys for all
* the groups the given file is a part of
*
* @param file
*
- * @returna a set of {@link GroupKey}s representing the group(s) the given
- * file is a part of
+ *
+ * @return A a set of GroupKeys representing the group(s) the given file is
+ * a part of.
*/
@SuppressWarnings({"rawtypes", "unchecked"})
synchronized public Set> getGroupKeysForFile(DrawableFile file) {
Set> resultSet = new HashSet<>();
- for (Comparable> val : groupBy.getValue(file)) {
- if (groupBy == DrawableAttribute.TAGS) {
+ for (Comparable> val : getGroupBy().getValue(file)) {
+ if (getGroupBy() == DrawableAttribute.TAGS) {
if (CategoryManager.isNotCategoryTagName((TagName) val)) {
- resultSet.add(new GroupKey(groupBy, val));
+ resultSet.add(new GroupKey(getGroupBy(), val, getDataSource()));
}
} else {
- resultSet.add(new GroupKey(groupBy, val));
+ resultSet.add(new GroupKey(getGroupBy(), val, getDataSource()));
}
}
return resultSet;
}
/**
- * using the current groupBy set for this manager, find groupkeys for all
- * the groups the given file is a part of
+ * Using the current grouping paramaters set for this manager, find
+ * GroupKeys for all the Groups the given file is a part of.
*
- * @return a a set of {@link GroupKey}s representing the group(s) the given
- * file is a part of
+ * @param fileID The Id of the file to get group keys for.
+ *
+ * @return A set of GroupKeys representing the group(s) the given file is a
+ * part of
*/
synchronized public Set> getGroupKeysForFileID(Long fileID) {
try {
- if (nonNull(db)) {
- DrawableFile file = db.getFileFromID(fileID);
- return getGroupKeysForFile(file);
- } else {
- Logger.getLogger(GroupManager.class.getName()).log(Level.WARNING, "Failed to load file with id: {0} from database. There is no database assigned.", fileID); //NON-NLS
- }
+ DrawableFile file = getDrawableDB().getFileFromID(fileID);
+ return getGroupKeysForFile(file);
} catch (TskCoreException ex) {
Logger.getLogger(GroupManager.class.getName()).log(Level.SEVERE, "failed to load file with id: " + fileID + " from database", ex); //NON-NLS
}
@@ -211,71 +203,68 @@ public class GroupManager {
* or null if no group exists for that key.
*/
@Nullable
- public DrawableGroup getGroupForKey(@Nonnull GroupKey> groupKey) {
- synchronized (groupMap) {
- return groupMap.get(groupKey);
- }
+ synchronized public DrawableGroup getGroupForKey(@Nonnull GroupKey> groupKey) {
+ return groupMap.get(groupKey);
}
- synchronized public void clear() {
+ synchronized public void reset() {
+ Platform.runLater(regrouper::cancel);
- if (groupByTask != null) {
- groupByTask.cancel(true);
- }
- sortBy = GroupSortBy.GROUP_BY_VALUE;
- groupBy = DrawableAttribute.PATH;
- sortOrder = SortOrder.ASCENDING;
- Platform.runLater(() -> {
- unSeenGroups.forEach(controller.getCategoryManager()::unregisterListener);
- unSeenGroups.clear();
- analyzedGroups.forEach(controller.getCategoryManager()::unregisterListener);
- analyzedGroups.clear();
+ setSortBy(GroupSortBy.GROUP_BY_VALUE);
+ setGroupBy(DrawableAttribute.PATH);
+ setSortOrder(SortOrder.ASCENDING);
+ setDataSource(null);
- });
- synchronized (groupMap) {
- groupMap.values().forEach(controller.getCategoryManager()::unregisterListener);
- groupMap.clear();
- }
- db = null;
+ unSeenGroups.forEach(controller.getCategoryManager()::unregisterListener);
+ unSeenGroups.clear();
+ analyzedGroups.forEach(controller.getCategoryManager()::unregisterListener);
+ analyzedGroups.clear();
+
+ groupMap.values().forEach(controller.getCategoryManager()::unregisterListener);
+ groupMap.clear();
}
public boolean isRegrouping() {
- if (groupByTask == null) {
- return false;
- }
+ Worker.State state = regrouper.getState();
+ return Arrays.asList(Worker.State.READY, Worker.State.RUNNING, Worker.State.SCHEDULED)
+ .contains(state);
+ }
- switch (groupByTask.getState()) {
- case READY:
- case RUNNING:
- case SCHEDULED:
- return true;
- case CANCELLED:
- case FAILED:
-
- case SUCCEEDED:
- default:
- return false;
- }
+ public ReadOnlyObjectProperty reGroupingState() {
+ return regrouper.stateProperty();
}
/**
- * 'mark' the given group as seen. This removes it from the queue of groups
- * to review, and is persisted in the drawable db.
+ * 'Save' the given group as seen in the drawable db.
+ *
+ * @param group The DrawableGroup to mark as seen.
+ * @param seen The seen state to set for the given group.
+ *
+ * @return A ListenableFuture that encapsulates saving the seen state to the
+ * DB.
+ *
*
- * @param group the {@link DrawableGroup} to mark as seen
*/
- @ThreadConfined(type = ThreadType.JFX)
- public void markGroupSeen(DrawableGroup group, boolean seen) {
- if (nonNull(db)) {
- db.markGroupSeen(group.getGroupKey(), seen);
- group.setSeen(seen);
- if (seen) {
- unSeenGroups.removeAll(group);
- } else if (unSeenGroups.contains(group) == false) {
- unSeenGroups.add(group);
+ public ListenableFuture> markGroupSeen(DrawableGroup group, boolean seen) {
+ return exec.submit(() -> {
+ try {
+ Examiner examiner = controller.getSleuthKitCase().getCurrentExaminer();
+ getDrawableDB().markGroupSeen(group.getGroupKey(), seen, examiner.getId());
+ group.setSeen(seen);
+ updateUnSeenGroups(group);
+ } catch (TskCoreException ex) {
+ logger.log(Level.SEVERE, "Error marking group as seen", ex); //NON-NLS
}
- FXCollections.sort(unSeenGroups, applySortOrder(sortOrder, sortBy));
+ });
+ }
+
+ synchronized private void updateUnSeenGroups(DrawableGroup group) {
+ if (group.isSeen()) {
+ unSeenGroups.removeAll(group);
+ } else if (unSeenGroups.contains(group) == false) {
+ unSeenGroups.add(group);
}
+ sortUnseenGroups();
}
/**
@@ -285,297 +274,216 @@ public class GroupManager {
*
* @param groupKey the value of groupKey
* @param fileID the value of file
+ *
+ * @return The DrawableGroup the file was removed from.
+ *
*/
public synchronized DrawableGroup removeFromGroup(GroupKey> groupKey, final Long fileID) {
//get grouping this file would be in
final DrawableGroup group = getGroupForKey(groupKey);
if (group != null) {
- Platform.runLater(() -> {
+ synchronized (group) {
group.removeFile(fileID);
- });
- // If we're grouping by category, we don't want to remove empty groups.
- if (groupKey.getAttribute() != DrawableAttribute.CATEGORY) {
- if (group.getFileIDs().isEmpty()) {
- Platform.runLater(() -> {
- if (analyzedGroups.contains(group)) {
- analyzedGroups.remove(group);
- FXCollections.sort(analyzedGroups, applySortOrder(sortOrder, sortBy));
- }
- if (unSeenGroups.contains(group)) {
- unSeenGroups.remove(group);
- FXCollections.sort(unSeenGroups, applySortOrder(sortOrder, sortBy));
- }
- });
+ // If we're grouping by category, we don't want to remove empty groups.
+ if (groupKey.getAttribute() != DrawableAttribute.CATEGORY
+ && group.getFileIDs().isEmpty()) {
+ if (analyzedGroups.contains(group)) {
+ analyzedGroups.remove(group);
+ sortAnalyzedGroups();
+ }
+ if (unSeenGroups.contains(group)) {
+ unSeenGroups.remove(group);
+ sortUnseenGroups();
+ }
+
}
- } else { //group == null
- // It may be that this was the last unanalyzed file in the group, so test
- // whether the group is now fully analyzed.
- popuplateIfAnalyzed(groupKey, null);
+ return group;
}
+ } else { //group == null
+ // It may be that this was the last unanalyzed file in the group, so test
+ // whether the group is now fully analyzed.
+ return popuplateIfAnalyzed(groupKey, null);
}
- return group;
}
- /**
- * find the distinct values for the given column (DrawableAttribute)
- *
- * These values represent the groups of files.
- *
- * @param groupBy
- *
- * @return
- */
- @SuppressWarnings({"unchecked"})
- public > List findValuesForAttribute(DrawableAttribute groupBy) {
- List values = Collections.emptyList();
- try {
- switch (groupBy.attrName) {
- //these cases get special treatment
- case CATEGORY:
- values = (List) Arrays.asList(DhsImageCategory.values());
- break;
- case TAGS:
- values = (List) controller.getTagsManager().getTagNamesInUse().stream()
- .filter(CategoryManager::isNotCategoryTagName)
- .collect(Collectors.toList());
- break;
- case ANALYZED:
- values = (List) Arrays.asList(false, true);
- break;
- case HASHSET:
- if (nonNull(db)) {
- TreeSet names = new TreeSet<>((Collection extends A>) db.getHashSetNames());
- values = new ArrayList<>(names);
- }
- break;
- case MIME_TYPE:
- if (nonNull(db)) {
- HashSet types = new HashSet<>();
-
- // Use the group_concat function to get a list of files for each mime type.
- // This has different syntax on Postgres vs SQLite
- String groupConcatClause;
- if (DbType.POSTGRESQL == controller.getSleuthKitCase().getDatabaseType()) {
- groupConcatClause = " array_to_string(array_agg(obj_id), ',') as object_ids";
- }
- else {
- groupConcatClause = " group_concat(obj_id) as object_ids";
- }
- String query = "select " + groupConcatClause + " , mime_type from tsk_files group by mime_type ";
- try (SleuthkitCase.CaseDbQuery executeQuery = controller.getSleuthKitCase().executeQuery(query); //NON-NLS
- ResultSet resultSet = executeQuery.getResultSet();) {
- while (resultSet.next()) {
- final String mimeType = resultSet.getString("mime_type"); //NON-NLS
- String objIds = resultSet.getString("object_ids"); //NON-NLS
-
- Pattern.compile(",").splitAsStream(objIds)
- .map(Long::valueOf)
- .filter(db::isInDB)
- .findAny().ifPresent(obj_id -> types.add(mimeType));
- }
- } catch (SQLException | TskCoreException ex) {
- Exceptions.printStackTrace(ex);
- }
- values = new ArrayList<>((Collection extends A>) types);
- }
- break;
- default:
- //otherwise do straight db query
- if (nonNull(db)) {
- values = db.findValuesForAttribute(groupBy, sortBy, sortOrder);
- }
- }
-
- return values;
- } catch (TskCoreException ex) {
- LOGGER.log(Level.WARNING, "TSK error getting list of type {0}", groupBy.getDisplayName()); //NON-NLS
- return Collections.emptyList();
+ synchronized private void sortUnseenGroups() {
+ if (isNotEmpty(unSeenGroups)) {
+ FXCollections.sort(unSeenGroups, makeGroupComparator(getSortOrder(), getSortBy()));
}
-
}
- public Set getFileIDsInGroup(GroupKey> groupKey) throws TskCoreException {
- Set fileIDsToReturn = Collections.emptySet();
+ synchronized private void sortAnalyzedGroups() {
+ if (isNotEmpty(analyzedGroups)) {
+ FXCollections.sort(analyzedGroups, makeGroupComparator(getSortOrder(), getSortBy()));
+ }
+ }
+
+ synchronized public Set getFileIDsInGroup(GroupKey> groupKey) throws TskCoreException {
+
switch (groupKey.getAttribute().attrName) {
//these cases get special treatment
case CATEGORY:
- fileIDsToReturn = getFileIDsWithCategory((DhsImageCategory) groupKey.getValue());
- break;
+ return getFileIDsWithCategory((DhsImageCategory) groupKey.getValue());
case TAGS:
- fileIDsToReturn = getFileIDsWithTag((TagName) groupKey.getValue());
- break;
+ return getFileIDsWithTag((TagName) groupKey.getValue());
case MIME_TYPE:
- fileIDsToReturn = getFileIDsWithMimeType((String) groupKey.getValue());
- break;
+ return getFileIDsWithMimeType((String) groupKey.getValue());
// case HASHSET: //comment out this case to use db functionality for hashsets
// return getFileIDsWithHashSetName((String) groupKey.getValue());
default:
//straight db query
- if (nonNull(db)) {
- fileIDsToReturn = db.getFileIDsInGroup(groupKey);
- }
+ return getDrawableDB().getFileIDsInGroup(groupKey);
}
- return fileIDsToReturn;
}
// @@@ This was kind of slow in the profiler. Maybe we should cache it.
// Unless the list of file IDs is necessary, use countFilesWithCategory() to get the counts.
- public Set getFileIDsWithCategory(DhsImageCategory category) throws TskCoreException {
+ synchronized public Set getFileIDsWithCategory(DhsImageCategory category) throws TskCoreException {
Set fileIDsToReturn = Collections.emptySet();
- if (nonNull(db)) {
- try {
- final DrawableTagsManager tagsManager = controller.getTagsManager();
- if (category == DhsImageCategory.ZERO) {
- List< TagName> tns = Stream.of(DhsImageCategory.ONE, DhsImageCategory.TWO, DhsImageCategory.THREE, DhsImageCategory.FOUR, DhsImageCategory.FIVE)
- .map(tagsManager::getTagName)
- .collect(Collectors.toList());
- Set files = new HashSet<>();
- for (TagName tn : tns) {
- if (tn != null) {
- List contentTags = tagsManager.getContentTagsByTagName(tn);
- files.addAll(contentTags.stream()
- .filter(ct -> ct.getContent() instanceof AbstractFile)
- .filter(ct -> db.isInDB(ct.getContent().getId()))
- .map(ct -> ct.getContent().getId())
- .collect(Collectors.toSet()));
- }
+ try {
+ final DrawableTagsManager tagsManager = controller.getTagsManager();
+ if (category == DhsImageCategory.ZERO) {
+ Set fileIDs = new HashSet<>();
+ for (TagName catTagName : tagsManager.getCategoryTagNames()) {
+ if (notEqual(catTagName.getDisplayName(), DhsImageCategory.ZERO.getDisplayName())) {
+ tagsManager.getContentTagsByTagName(catTagName).stream()
+ .filter(ct -> ct.getContent() instanceof AbstractFile)
+ .map(ct -> ct.getContent().getId())
+ .filter(getDrawableDB()::isInDB)
+ .forEach(fileIDs::add);
}
-
- fileIDsToReturn = db.findAllFileIdsWhere("obj_id NOT IN (" + StringUtils.join(files, ',') + ")"); //NON-NLS
- } else {
-
- List contentTags = tagsManager.getContentTagsByTagName(tagsManager.getTagName(category));
- fileIDsToReturn = contentTags.stream()
- .filter(ct -> ct.getContent() instanceof AbstractFile)
- .filter(ct -> db.isInDB(ct.getContent().getId()))
- .map(ct -> ct.getContent().getId())
- .collect(Collectors.toSet());
}
- } catch (TskCoreException ex) {
- LOGGER.log(Level.WARNING, "TSK error getting files in Category:" + category.getDisplayName(), ex); //NON-NLS
- throw ex;
+
+ fileIDsToReturn = getDrawableDB().findAllFileIdsWhere("obj_id NOT IN (" + StringUtils.join(fileIDs, ',') + ")"); //NON-NLS
+ } else {
+
+ List contentTags = tagsManager.getContentTagsByTagName(tagsManager.getTagName(category));
+ fileIDsToReturn = contentTags.stream()
+ .filter(ct -> ct.getContent() instanceof AbstractFile)
+ .filter(ct -> getDrawableDB().isInDB(ct.getContent().getId()))
+ .map(ct -> ct.getContent().getId())
+ .collect(Collectors.toSet());
}
+ } catch (TskCoreException ex) {
+ logger.log(Level.WARNING, "TSK error getting files in Category:" + category.getDisplayName(), ex); //NON-NLS
+ throw ex;
}
+
return fileIDsToReturn;
}
- public Set getFileIDsWithTag(TagName tagName) throws TskCoreException {
- try {
- Set files = new HashSet<>();
- List contentTags = controller.getTagsManager().getContentTagsByTagName(tagName);
- for (ContentTag ct : contentTags) {
- if (ct.getContent() instanceof AbstractFile && nonNull(db) && db.isInDB(ct.getContent().getId())) {
- files.add(ct.getContent().getId());
- }
- }
- return files;
- } catch (TskCoreException ex) {
- LOGGER.log(Level.WARNING, "TSK error getting files with Tag:" + tagName.getDisplayName(), ex); //NON-NLS
- throw ex;
- }
+ synchronized public Set getFileIDsWithTag(TagName tagName) throws TskCoreException {
+ return controller.getTagsManager().getContentTagsByTagName(tagName).stream()
+ .map(ContentTag::getContent)
+ .filter(AbstractFile.class::isInstance)
+ .map(Content::getId)
+ .filter(getDrawableDB()::isInDB)
+ .collect(Collectors.toSet());
}
- public GroupSortBy getSortBy() {
- return sortBy;
+ public synchronized GroupSortBy getSortBy() {
+ return sortByProp.get();
}
- void setSortBy(GroupSortBy sortBy) {
- this.sortBy = sortBy;
- Platform.runLater(() -> sortByProp.set(sortBy));
+ synchronized void setSortBy(GroupSortBy sortBy) {
+ sortByProp.set(sortBy);
}
- public ReadOnlyObjectProperty< Comparator> getSortByProperty() {
+ public ReadOnlyObjectProperty< GroupSortBy> getSortByProperty() {
return sortByProp.getReadOnlyProperty();
}
- public DrawableAttribute> getGroupBy() {
- return groupBy;
+ public synchronized DrawableAttribute> getGroupBy() {
+ return groupByProp.get();
}
- void setGroupBy(DrawableAttribute> groupBy) {
- this.groupBy = groupBy;
- Platform.runLater(() -> groupByProp.set(groupBy));
+ synchronized void setGroupBy(DrawableAttribute> groupBy) {
+ groupByProp.set(groupBy);
}
public ReadOnlyObjectProperty> getGroupByProperty() {
return groupByProp.getReadOnlyProperty();
}
- public SortOrder getSortOrder() {
- return sortOrder;
+ public synchronized SortOrder getSortOrder() {
+ return sortOrderProp.get();
}
- void setSortOrder(SortOrder sortOrder) {
- this.sortOrder = sortOrder;
- Platform.runLater(() -> sortOrderProp.set(sortOrder));
+ synchronized void setSortOrder(SortOrder sortOrder) {
+ sortOrderProp.set(sortOrder);
}
public ReadOnlyObjectProperty getSortOrderProperty() {
return sortOrderProp.getReadOnlyProperty();
}
+ public synchronized DataSource getDataSource() {
+ return dataSourceProp.get();
+ }
+
+ synchronized void setDataSource(DataSource dataSource) {
+ dataSourceProp.set(dataSource);
+ }
+
+ public ReadOnlyObjectProperty getDataSourceProperty() {
+ return dataSourceProp.getReadOnlyProperty();
+ }
+
/**
- * regroup all files in the database using given {@link DrawableAttribute}
- * see {@link ReGroupTask} for more details.
+ * Regroup all files in the database. see ReGroupTask for more details.
*
- * @param groupBy
- * @param sortBy
- * @param sortOrder
- * @param force true to force a full db query regroup
+ * @param The type of the values of the groupBy attriubte.
+ * @param dataSource The DataSource to show. Null for all data sources.
+ * @param groupBy The DrawableAttribute to group by
+ * @param sortBy The GroupSortBy to sort the groups by
+ * @param sortOrder The SortOrder to use when sorting the groups.
+ * @param force true to force a full db query regroup, even if only the
+ * sorting has changed.
*/
- public synchronized > void regroup(final DrawableAttribute groupBy, final GroupSortBy sortBy, final SortOrder sortOrder, Boolean force) {
+ public synchronized > void regroup(DataSource dataSource, DrawableAttribute groupBy, GroupSortBy sortBy, SortOrder sortOrder, Boolean force) {
if (!Case.isCaseOpen()) {
return;
}
- //only re-query the db if the group by attribute changed or it is forced
- if (groupBy != getGroupBy() || force == true) {
+ //only re-query the db if the data source or group by attribute changed or it is forced
+ if (dataSource != getDataSource()
+ || groupBy != getGroupBy()
+ || force) {
+
+ setDataSource(dataSource);
setGroupBy(groupBy);
setSortBy(sortBy);
setSortOrder(sortOrder);
- if (groupByTask != null) {
- groupByTask.cancel(true);
- }
-
- groupByTask = new ReGroupTask<>(groupBy, sortBy, sortOrder);
- Platform.runLater(() -> regroupProgress.bind(groupByTask.progressProperty()));
- regroupExecutor.submit(groupByTask);
+ Platform.runLater(regrouper::restart);
} else {
// resort the list of groups
setSortBy(sortBy);
setSortOrder(sortOrder);
- Platform.runLater(() -> {
- FXCollections.sort(analyzedGroups, applySortOrder(sortOrder, sortBy));
- FXCollections.sort(unSeenGroups, applySortOrder(sortOrder, sortBy));
- });
+ sortAnalyzedGroups();
+ sortUnseenGroups();
}
}
- /**
- * an executor to submit async ui related background tasks to.
- */
- final ExecutorService regroupExecutor = Executors.newSingleThreadExecutor(new BasicThreadFactory.Builder().namingPattern("ui task -%d").build()); //NON-NLS
-
public ReadOnlyDoubleProperty regroupProgress() {
- return regroupProgress.getReadOnlyProperty();
+ return regrouper.progressProperty();
}
@Subscribe
- public void handleTagAdded(ContentTagAddedEvent evt) {
+ synchronized public void handleTagAdded(ContentTagAddedEvent evt) {
GroupKey> newGroupKey = null;
final long fileID = evt.getAddedTag().getContent().getId();
- if (groupBy == DrawableAttribute.CATEGORY && CategoryManager.isCategoryTagName(evt.getAddedTag().getName())) {
- newGroupKey = new GroupKey<>(DrawableAttribute.CATEGORY, CategoryManager.categoryFromTagName(evt.getAddedTag().getName()));
+ if (getGroupBy() == DrawableAttribute.CATEGORY && CategoryManager.isCategoryTagName(evt.getAddedTag().getName())) {
+ newGroupKey = new GroupKey<>(DrawableAttribute.CATEGORY, CategoryManager.categoryFromTagName(evt.getAddedTag().getName()), getDataSource());
for (GroupKey> oldGroupKey : groupMap.keySet()) {
if (oldGroupKey.equals(newGroupKey) == false) {
removeFromGroup(oldGroupKey, fileID);
}
}
- } else if (groupBy == DrawableAttribute.TAGS && CategoryManager.isNotCategoryTagName(evt.getAddedTag().getName())) {
- newGroupKey = new GroupKey<>(DrawableAttribute.TAGS, evt.getAddedTag().getName());
+ } else if (getGroupBy() == DrawableAttribute.TAGS && CategoryManager.isNotCategoryTagName(evt.getAddedTag().getName())) {
+ newGroupKey = new GroupKey<>(DrawableAttribute.TAGS, evt.getAddedTag().getName(), getDataSource());
}
if (newGroupKey != null) {
DrawableGroup g = getGroupForKey(newGroupKey);
@@ -584,27 +492,26 @@ public class GroupManager {
}
@SuppressWarnings("AssignmentToMethodParameter")
- private void addFileToGroup(DrawableGroup g, final GroupKey> groupKey, final long fileID) {
- if (g == null) {
+ synchronized private void addFileToGroup(DrawableGroup group, final GroupKey> groupKey, final long fileID) {
+ if (group == null) {
//if there wasn't already a group check if there should be one now
- g = popuplateIfAnalyzed(groupKey, null);
+ group = popuplateIfAnalyzed(groupKey, null);
}
- DrawableGroup group = g;
if (group != null) {
//if there is aleady a group that was previously deemed fully analyzed, then add this newly analyzed file to it.
- Platform.runLater(() -> group.addFile(fileID));
+ group.addFile(fileID);
}
}
@Subscribe
- public void handleTagDeleted(ContentTagDeletedEvent evt) {
+ synchronized public void handleTagDeleted(ContentTagDeletedEvent evt) {
GroupKey> groupKey = null;
final ContentTagDeletedEvent.DeletedContentTagInfo deletedTagInfo = evt.getDeletedTagInfo();
final TagName tagName = deletedTagInfo.getName();
- if (groupBy == DrawableAttribute.CATEGORY && CategoryManager.isCategoryTagName(tagName)) {
- groupKey = new GroupKey<>(DrawableAttribute.CATEGORY, CategoryManager.categoryFromTagName(tagName));
- } else if (groupBy == DrawableAttribute.TAGS && CategoryManager.isNotCategoryTagName(tagName)) {
- groupKey = new GroupKey<>(DrawableAttribute.TAGS, tagName);
+ if (getGroupBy() == DrawableAttribute.CATEGORY && CategoryManager.isCategoryTagName(tagName)) {
+ groupKey = new GroupKey<>(DrawableAttribute.CATEGORY, CategoryManager.categoryFromTagName(tagName), getDataSource());
+ } else if (getGroupBy() == DrawableAttribute.TAGS && CategoryManager.isNotCategoryTagName(tagName)) {
+ groupKey = new GroupKey<>(DrawableAttribute.TAGS, tagName, getDataSource());
}
if (groupKey != null) {
final long fileID = deletedTagInfo.getContentID();
@@ -626,10 +533,9 @@ public class GroupManager {
}
/**
- * handle {@link FileUpdateEvent} sent from Db when files are
- * inserted/updated
+ * Handle notifications sent from Db when files are inserted/updated
*
- * @param evt
+ * @param updatedFileIDs The ID of the inserted/updated files.
*/
@Subscribe
synchronized public void handleFileUpdate(Collection updatedFileIDs) {
@@ -654,67 +560,60 @@ public class GroupManager {
controller.getCategoryManager().fireChange(updatedFileIDs, null);
}
- private DrawableGroup popuplateIfAnalyzed(GroupKey> groupKey, ReGroupTask> task) {
+ synchronized private DrawableGroup popuplateIfAnalyzed(GroupKey> groupKey, ReGroupTask> task) {
+ /*
+ * If this method call is part of a ReGroupTask and that task is
+ * cancelled, no-op.
+ *
+ * This allows us to stop if a regroup task has been cancelled (e.g. the
+ * user picked a different group by attribute, while the current task
+ * was still running)
+ */
+ if (isNull(task) || task.isCancelled() == false) {
- if (Objects.nonNull(task) && (task.isCancelled())) {
/*
- * if this method call is part of a ReGroupTask and that task is
- * cancelled, no-op
- *
- * this allows us to stop if a regroup task has been cancelled (e.g.
- * the user picked a different group by attribute, while the current
- * task was still running)
+ * For attributes other than path we can't be sure a group is fully
+ * analyzed because we don't know all the files that will be a part
+ * of that group. just show them no matter what.
*/
-
- } else // no task or un-cancelled task
- {
- if (nonNull(db) && ((groupKey.getAttribute() != DrawableAttribute.PATH) || db.isGroupAnalyzed(groupKey))) {
- /*
- * for attributes other than path we can't be sure a group is
- * fully analyzed because we don't know all the files that will
- * be a part of that group,. just show them no matter what.
- */
-
+ if (groupKey.getAttribute() != DrawableAttribute.PATH
+ || getDrawableDB().isGroupAnalyzed(groupKey)) {
try {
Set fileIDs = getFileIDsInGroup(groupKey);
if (Objects.nonNull(fileIDs)) {
+
+ long examinerID = collaborativeModeProp.get() ? -1 : controller.getSleuthKitCase().getCurrentExaminer().getId();
+ final boolean groupSeen = getDrawableDB().isGroupSeenByExaminer(groupKey, examinerID);
DrawableGroup group;
- final boolean groupSeen = db.isGroupSeen(groupKey);
- synchronized (groupMap) {
- if (groupMap.containsKey(groupKey)) {
- group = groupMap.get(groupKey);
- group.setFiles(ObjectUtils.defaultIfNull(fileIDs, Collections.emptySet()));
- } else {
- group = new DrawableGroup(groupKey, fileIDs, groupSeen);
- controller.getCategoryManager().registerListener(group);
- group.seenProperty().addListener((o, oldSeen, newSeen) ->
- Platform.runLater(() -> markGroupSeen(group, newSeen))
- );
- groupMap.put(groupKey, group);
- }
+ if (groupMap.containsKey(groupKey)) {
+ group = groupMap.get(groupKey);
+ group.setFiles(fileIDs);
+ group.setSeen(groupSeen);
+ } else {
+ group = new DrawableGroup(groupKey, fileIDs, groupSeen);
+ controller.getCategoryManager().registerListener(group);
+ groupMap.put(groupKey, group);
}
- Platform.runLater(() -> {
- if (analyzedGroups.contains(group) == false) {
- analyzedGroups.add(group);
- if (Objects.isNull(task)) {
- FXCollections.sort(analyzedGroups, applySortOrder(sortOrder, sortBy));
- }
- }
- markGroupSeen(group, groupSeen);
- });
- return group;
+ if (analyzedGroups.contains(group) == false) {
+ analyzedGroups.add(group);
+ sortAnalyzedGroups();
+ }
+ updateUnSeenGroups(group);
+
+ return group;
}
} catch (TskCoreException ex) {
- LOGGER.log(Level.SEVERE, "failed to get files for group: " + groupKey.getAttribute().attrName.toString() + " = " + groupKey.getValue(), ex); //NON-NLS
+ logger.log(Level.SEVERE, "failed to get files for group: " + groupKey.getAttribute().attrName.toString() + " = " + groupKey.getValue(), ex); //NON-NLS
}
}
}
+
return null;
}
- public Set getFileIDsWithMimeType(String mimeType) throws TskCoreException {
+ synchronized public Set getFileIDsWithMimeType(String mimeType) throws TskCoreException {
HashSet hashSet = new HashSet<>();
String query = (null == mimeType)
@@ -725,21 +624,45 @@ public class GroupManager {
ResultSet resultSet = executeQuery.getResultSet();) {
while (resultSet.next()) {
final long fileID = resultSet.getLong("obj_id"); //NON-NLS
- if (nonNull(db) && db.isInDB(fileID)) {
+ if (getDrawableDB().isInDB(fileID)) {
hashSet.add(fileID);
}
}
return hashSet;
} catch (Exception ex) {
- Exceptions.printStackTrace(ex);
throw new TskCoreException("Failed to get file ids with mime type " + mimeType, ex);
}
}
+ synchronized public void setCollaborativeMode(Boolean newValue) {
+ collaborativeModeProp.set(newValue);
+ analyzedGroups.forEach(group -> {
+ try {
+ boolean groupSeenByExaminer = getDrawableDB().isGroupSeenByExaminer(
+ group.getGroupKey(),
+ newValue ? -1 : controller.getSleuthKitCase().getCurrentExaminer().getId()
+ );
+ group.setSeen(groupSeenByExaminer);
+ updateUnSeenGroups(group);
+ if (group.isSeen()) {
+ unSeenGroups.removeAll(group);
+ } else if (unSeenGroups.contains(group) == false) {
+ unSeenGroups.add(group);
+ }
+
+ } catch (TskCoreException ex) {
+ logger.log(Level.SEVERE, "Error checking seen state of group.", ex);
+ }
+ });
+ sortUnseenGroups();
+ }
+
/**
* Task to query database for files in sorted groups and build
- * {@link Groupings} for them
+ * DrawableGroups for them.
+ *
+ * @param The type of the values that this task will group by.
*/
@SuppressWarnings({"unchecked", "rawtypes"})
@NbBundle.Messages({"# {0} - groupBy attribute Name",
@@ -749,76 +672,192 @@ public class GroupManager {
"# {0} - groupBy attribute Name",
"# {1} - atribute value",
"ReGroupTask.progressUpdate=regrouping files by {0} : {1}"})
- private class ReGroupTask> extends LoggedTask {
-
- private ProgressHandle groupProgress;
-
- private final DrawableAttribute groupBy;
+ class ReGroupTask> extends LoggedTask {
+ private final DataSource dataSource;
+ private final DrawableAttribute groupBy;
private final GroupSortBy sortBy;
-
private final SortOrder sortOrder;
- ReGroupTask(DrawableAttribute groupBy, GroupSortBy sortBy, SortOrder sortOrder) {
- super(Bundle.ReGroupTask_displayTitle(groupBy.attrName.toString(), sortBy.getDisplayName(), sortOrder.toString()), true);
+ private final ProgressHandle groupProgress;
+ ReGroupTask(DataSource dataSource, DrawableAttribute groupBy, GroupSortBy sortBy, SortOrder sortOrder) {
+ super(Bundle.ReGroupTask_displayTitle(groupBy.attrName.toString(), sortBy.getDisplayName(), sortOrder.toString()), true);
+ this.dataSource = dataSource;
this.groupBy = groupBy;
this.sortBy = sortBy;
this.sortOrder = sortOrder;
- }
- @Override
- public boolean isCancelled() {
- return super.isCancelled() || groupBy != getGroupBy() || sortBy != getSortBy() || sortOrder != getSortOrder();
+ groupProgress = ProgressHandle.createHandle(Bundle.ReGroupTask_displayTitle(groupBy.attrName.toString(), sortBy.getDisplayName(), sortOrder.toString()), this);
}
@Override
protected Void call() throws Exception {
+ try {
+ if (isCancelled()) {
+ return null;
+ }
+ groupProgress.start();
- if (isCancelled()) {
- return null;
- }
-
- groupProgress = ProgressHandle.createHandle(Bundle.ReGroupTask_displayTitle(groupBy.attrName.toString(), sortBy.getDisplayName(), sortOrder.toString()), this);
- Platform.runLater(() -> {
analyzedGroups.clear();
unSeenGroups.clear();
- });
- // Get the list of group keys
- final List vals = findValuesForAttribute(groupBy);
+ // Get the list of group keys
+ Multimap valsByDataSource = findValuesForAttribute();
- groupProgress.start(vals.size());
-
- int p = 0;
- // For each key value, partially create the group and add it to the list.
- for (final AttrType val : vals) {
- if (isCancelled()) {
- return null;//abort
+ groupProgress.switchToDeterminate(valsByDataSource.entries().size());
+ int p = 0;
+ // For each key value, partially create the group and add it to the list.
+ for (final Map.Entry val : valsByDataSource.entries()) {
+ if (isCancelled()) {
+ return null;
+ }
+ p++;
+ updateMessage(Bundle.ReGroupTask_progressUpdate(groupBy.attrName.toString(), val.getValue()));
+ updateProgress(p, valsByDataSource.size());
+ groupProgress.progress(Bundle.ReGroupTask_progressUpdate(groupBy.attrName.toString(), val), p);
+ popuplateIfAnalyzed(new GroupKey<>(groupBy, val.getValue(), val.getKey()), this);
}
- p++;
- updateMessage(Bundle.ReGroupTask_progressUpdate(groupBy.attrName.toString(), val));
- updateProgress(p, vals.size());
- groupProgress.progress(Bundle.ReGroupTask_progressUpdate(groupBy.attrName.toString(), val), p);
- popuplateIfAnalyzed(new GroupKey<>(groupBy, val), this);
- }
- Platform.runLater(() -> FXCollections.sort(analyzedGroups, applySortOrder(sortOrder, sortBy)));
- updateProgress(1, 1);
+ Optional viewedGroup
+ = Optional.ofNullable(controller.getViewState())
+ .flatMap(GroupViewState::getGroup);
+ Optional> viewedKey = viewedGroup.map(DrawableGroup::getGroupKey);
+ DataSource dataSourceOfCurrentGroup
+ = viewedKey.flatMap(GroupKey::getDataSource)
+ .orElse(null);
+ DrawableAttribute attributeOfCurrentGroup
+ = viewedKey.map(GroupKey::getAttribute)
+ .orElse(null);
+ /* if no group or if groupbies are different or if data source
+ * != null and does not equal group */
+ if (viewedGroup.isPresent() == false) {
+
+ //the current group should not be visible so ...
+ if (isNotEmpty(unSeenGroups)) {// show then next unseen group
+ controller.advance(GroupViewState.tile(unSeenGroups.get(0)));
+ } else if (isNotEmpty(analyzedGroups)) {
+ //show the first analyzed group.
+ controller.advance(GroupViewState.tile(analyzedGroups.get(0)));
+ } else { //there are no groups, clear the group area.
+ controller.advance(GroupViewState.tile(null));
+ }
+ } else if ((getDataSource() != null && notEqual(dataSourceOfCurrentGroup, getDataSource()))) {
+
+ //the current group should not be visible so ...
+ if (isNotEmpty(unSeenGroups)) {// show then next unseen group
+ controller.advance(GroupViewState.tile(unSeenGroups.get(0)));
+ } else if (isNotEmpty(analyzedGroups)) {
+ //show the first analyzed group.
+ controller.advance(GroupViewState.tile(analyzedGroups.get(0)));
+ } else { //there are no groups, clear the group area.
+ controller.advance(GroupViewState.tile(null));
+ }
+ } else if (getGroupBy() != attributeOfCurrentGroup) {
+ //the current group should not be visible so ...
+ if (isNotEmpty(unSeenGroups)) {// show then next unseen group
+ controller.advance(GroupViewState.tile(unSeenGroups.get(0)));
+ } else if (isNotEmpty(analyzedGroups)) {
+ //show the first analyzed group.
+ controller.advance(GroupViewState.tile(analyzedGroups.get(0)));
+ } else { //there are no groups, clear the group area.
+ controller.advance(GroupViewState.tile(null));
+ }
+ }
+ } finally {
+ groupProgress.finish();
+ updateProgress(1, 1);
+ }
return null;
}
@Override
protected void done() {
super.done();
- if (groupProgress != null) {
- groupProgress.finish();
- groupProgress = null;
+ try {
+ get();
+ } catch (CancellationException cancelEx) { //NOPMD
+ //cancellation is normal
+ } catch (InterruptedException | ExecutionException ex) {
+ logger.log(Level.SEVERE, "Error while regrouping.", ex);
}
}
+
+ /**
+ * find the distinct values for the given column (DrawableAttribute)
+ *
+ * These values represent the groups of files.
+ *
+ * @param groupBy
+ *
+ * @return
+ */
+ public Multimap findValuesForAttribute() {
+
+ Multimap results = HashMultimap.create();
+ try {
+ switch (groupBy.attrName) {
+ //these cases get special treatment
+ case CATEGORY:
+ results.putAll(null, Arrays.asList(DhsImageCategory.values()));
+ break;
+ case TAGS:
+ results.putAll(null, controller.getTagsManager().getTagNamesInUse().stream()
+ .filter(CategoryManager::isNotCategoryTagName)
+ .collect(Collectors.toList()));
+ break;
+
+ case ANALYZED:
+ results.putAll(null, Arrays.asList(false, true));
+ break;
+ case HASHSET:
+
+ results.putAll(null, new TreeSet<>(getDrawableDB().getHashSetNames()));
+
+ break;
+ case MIME_TYPE:
+
+ HashSet types = new HashSet<>();
+
+ // Use the group_concat function to get a list of files for each mime type.
+ // This has different syntax on Postgres vs SQLite
+ String groupConcatClause;
+ if (DbType.POSTGRESQL == controller.getSleuthKitCase().getDatabaseType()) {
+ groupConcatClause = " array_to_string(array_agg(obj_id), ',') as object_ids";
+ } else {
+ groupConcatClause = " group_concat(obj_id) as object_ids";
+ }
+ String query = "select " + groupConcatClause + " , mime_type from tsk_files group by mime_type ";
+ try (SleuthkitCase.CaseDbQuery executeQuery = controller.getSleuthKitCase().executeQuery(query); //NON-NLS
+ ResultSet resultSet = executeQuery.getResultSet();) {
+ while (resultSet.next()) {
+ final String mimeType = resultSet.getString("mime_type"); //NON-NLS
+ String objIds = resultSet.getString("object_ids"); //NON-NLS
+
+ Pattern.compile(",").splitAsStream(objIds)
+ .map(Long::valueOf)
+ .filter(getDrawableDB()::isInDB)
+ .findAny().ifPresent(obj_id -> types.add(mimeType));
+ }
+ } catch (SQLException | TskCoreException ex) {
+ Exceptions.printStackTrace(ex);
+ }
+ results.putAll(null, types);
+
+ break;
+ default:
+ //otherwise do straight db query
+ results.putAll(getDrawableDB().findValuesForAttribute(groupBy, sortBy, sortOrder, dataSource));
+ }
+ } catch (TskCoreException ex) {
+ logger.log(Level.SEVERE, "TSK error getting list of type {0}", groupBy.getDisplayName()); //NON-NLS
+ }
+ return results;
+
+ }
}
- private static Comparator applySortOrder(final SortOrder sortOrder, Comparator comparator) {
+ private static Comparator makeGroupComparator(final SortOrder sortOrder, GroupSortBy comparator) {
switch (sortOrder) {
case ASCENDING:
return comparator;
@@ -829,4 +868,21 @@ public class GroupManager {
return new GroupSortBy.AllEqualComparator<>();
}
}
+
+ /**
+ * @return the drawableDB
+ */
+ private DrawableDB getDrawableDB() {
+ return controller.getDatabase();
+ }
+
+ class GroupingService extends Service< Void> {
+
+ @Override
+ protected Task createTask() {
+ synchronized (GroupManager.this) {
+ return new ReGroupTask<>(getDataSource(), getGroupBy(), getSortBy(), getSortOrder());
+ }
+ }
+ }
}
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupSortBy.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupSortBy.java
index 65e1870de8..97a75f0f5b 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupSortBy.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupSortBy.java
@@ -28,7 +28,8 @@ import org.openide.util.NbBundle;
/**
* 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.none=None",
"GroupSortBy.priority=Priority"})
@@ -37,40 +38,35 @@ public class GroupSortBy implements Comparator {
/**
* sort the groups by the number of files in each
*/
- public final static GroupSortBy FILE_COUNT =
- new GroupSortBy(Bundle.GroupSortBy_groupSize(), "folder-open-image.png",
+ public final static GroupSortBy FILE_COUNT
+ = new GroupSortBy(Bundle.GroupSortBy_groupSize(), "folder-open-image.png",
Comparator.comparing(DrawableGroup::getSize));
/**
* sort the groups by the natural order of the grouping value ( eg group
* them by path alphabetically )
*/
- public final static GroupSortBy GROUP_BY_VALUE =
- new GroupSortBy(Bundle.GroupSortBy_groupName(), "folder-rename.png",
+ public final static GroupSortBy GROUP_BY_VALUE
+ = new GroupSortBy(Bundle.GroupSortBy_groupName(), "folder-rename.png",
Comparator.comparing(DrawableGroup::getGroupByValueDislpayName));
/**
* don't sort the groups just use what ever order they come in (ingest
* order)
*/
- public final static GroupSortBy NONE =
- new GroupSortBy(Bundle.GroupSortBy_none(), "prohibition.png",
+ public final static GroupSortBy NONE
+ = new GroupSortBy(Bundle.GroupSortBy_none(), "prohibition.png",
new AllEqualComparator<>());
/**
* sort the groups by some priority metric to be determined and implemented
*/
- public final static GroupSortBy PRIORITY =
- new GroupSortBy(Bundle.GroupSortBy_priority(), "hashset_hits.png",
+ public final static GroupSortBy PRIORITY
+ = new GroupSortBy(Bundle.GroupSortBy_priority(), "hashset_hits.png",
Comparator.comparing(DrawableGroup::getHashHitDensity)
.thenComparing(Comparator.comparing(DrawableGroup::getUncategorizedCount))
.reversed());
- @Override
- public int compare(DrawableGroup o1, DrawableGroup o2) {
- return delegate.compare(o1, o2);
- }
-
private final static ObservableList values = FXCollections.unmodifiableObservableList(FXCollections.observableArrayList(PRIORITY, NONE, GROUP_BY_VALUE, FILE_COUNT));
/**
@@ -109,6 +105,11 @@ public class GroupSortBy implements Comparator {
return icon;
}
+ @Override
+ public int compare(DrawableGroup o1, DrawableGroup o2) {
+ return delegate.compare(o1, o2);
+ }
+
static class AllEqualComparator implements Comparator {
@Override
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupViewState.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupViewState.java
index 86679340ab..fa578021ef 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupViewState.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupViewState.java
@@ -22,9 +22,9 @@ import java.util.Objects;
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;
@@ -32,8 +32,8 @@ public class GroupViewState {
private final Optional slideShowfileID;
- public DrawableGroup getGroup() {
- return group;
+ public Optional getGroup() {
+ return Optional.ofNullable(group);
}
public GroupViewMode getMode() {
@@ -44,18 +44,18 @@ public class GroupViewState {
return slideShowfileID;
}
- private GroupViewState(DrawableGroup g, GroupViewMode mode, Long slideShowfileID) {
- this.group = g;
+ private GroupViewState(DrawableGroup group, GroupViewMode mode, Long slideShowfileID) {
+ this.group = group;
this.mode = mode;
this.slideShowfileID = Optional.ofNullable(slideShowfileID);
}
- public static GroupViewState tile(DrawableGroup g) {
- return new GroupViewState(g, GroupViewMode.TILE, null);
+ public static GroupViewState tile(DrawableGroup group) {
+ return new GroupViewState(group, GroupViewMode.TILE, null);
}
- public static GroupViewState slideShow(DrawableGroup g, Long fileID) {
- return new GroupViewState(g, GroupViewMode.SLIDE_SHOW, fileID);
+ public static GroupViewState slideShow(DrawableGroup group, Long fileID) {
+ return new GroupViewState(group, GroupViewMode.SLIDE_SHOW, fileID);
}
@Override
@@ -82,10 +82,7 @@ public class GroupViewState {
if (this.mode != other.mode) {
return false;
}
- if (!Objects.equals(this.slideShowfileID, other.slideShowfileID)) {
- return false;
- }
- return true;
+ return Objects.equals(this.slideShowfileID, other.slideShowfileID);
}
}
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/GuiUtils.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/GuiUtils.java
index 2ff6f24d90..91b55e5bf1 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/GuiUtils.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/GuiUtils.java
@@ -18,20 +18,43 @@
*/
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.Dialog;
import javafx.scene.control.MenuItem;
+import javafx.scene.image.Image;
+import javafx.stage.Stage;
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
*/
-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() {
}
/**
- * 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
* remember the last chosen menu item as its action.
*
@@ -51,4 +74,14 @@ public class GuiUtils {
});
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);
+ }
}
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/StatusBar.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/StatusBar.java
index 143ef142fd..2355d6d2a0 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/StatusBar.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/StatusBar.java
@@ -79,7 +79,7 @@ public class StatusBar extends AnchorPane {
});
Platform.runLater(() -> staleLabel.setTooltip(new Tooltip(Bundle.StatuBar_toolTip())));
- staleLabel.visibleProperty().bind(controller.stale());
+ staleLabel.visibleProperty().bind(controller.staleProperty());
}
public StatusBar(ImageGalleryController controller) {
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SummaryTablePane.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SummaryTablePane.java
index 44814e2c9a..a9c4da2907 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SummaryTablePane.java
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SummaryTablePane.java
@@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
- * Copyright 2013-15 Basis Technology Corp.
+ * Copyright 2013-18 Basis Technology Corp.
* Contact: carrier sleuthkit org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -34,9 +34,10 @@ import javafx.scene.layout.VBox;
import javafx.util.Pair;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.datamodel.DhsImageCategory;
import org.sleuthkit.autopsy.imagegallery.FXMLConstructor;
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
@@ -51,11 +52,13 @@ public class SummaryTablePane extends AnchorPane {
@FXML
private TableView> tableView;
+
private final ImageGalleryController controller;
@FXML
- @NbBundle.Messages({"SummaryTablePane.catColumn=Category",
- "SummaryTablePane.countColumn=# Files"})
+ @NbBundle.Messages({
+ "SummaryTablePane.catColumn=Category",
+ "SummaryTablePane.countColumn=# Files"})
void initialize() {
assert catColumn != null : "fx:id=\"catColumn\" was not injected: check your FXML file 'SummaryTablePane.fxml'.";
assert countColumn != null : "fx:id=\"countColumn\" was not injected: check your FXML file 'SummaryTablePane.fxml'.";
@@ -67,11 +70,11 @@ public class SummaryTablePane extends AnchorPane {
tableView.prefHeightProperty().set(7 * 25);
//set up columns
- catColumn.setCellValueFactory((TableColumn.CellDataFeatures, String> p) -> new SimpleObjectProperty<>(p.getValue().getKey().getDisplayName()));
+ catColumn.setCellValueFactory(params -> new SimpleObjectProperty<>(params.getValue().getKey().getDisplayName()));
catColumn.setPrefWidth(USE_COMPUTED_SIZE);
catColumn.setText(Bundle.SummaryTablePane_catColumn());
- countColumn.setCellValueFactory((TableColumn.CellDataFeatures, Long> p) -> new SimpleObjectProperty<>(p.getValue().getValue()));
+ countColumn.setCellValueFactory(params -> new SimpleObjectProperty<>(params.getValue().getValue()));
countColumn.setPrefWidth(USE_COMPUTED_SIZE);
countColumn.setText(Bundle.SummaryTablePane_countColumn());
@@ -85,14 +88,15 @@ public class SummaryTablePane extends AnchorPane {
public SummaryTablePane(ImageGalleryController controller) {
this.controller = controller;
FXMLConstructor.construct(this, "SummaryTablePane.fxml"); //NON-NLS
-
}
/**
* listen to Category updates and rebuild the table
+ *
+ * @param evt The change event.
*/
@Subscribe
- public void handleCategoryChanged(org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager.CategoryChangeEvent evt) {
+ public void handleCategoryChanged(CategoryChangeEvent evt) {
final ObservableList> data = FXCollections.observableArrayList();
if (Case.isCaseOpen()) {
for (DhsImageCategory cat : DhsImageCategory.values()) {
diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/Toolbar.fxml b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/Toolbar.fxml
index a8ced6deda..77f73b2cb9 100644
--- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/Toolbar.fxml
+++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/Toolbar.fxml
@@ -12,29 +12,41 @@
-
-
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
-
+