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> 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) 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) 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 @@ - - + + - - + + + + + + + + + + + + + - - - - - - + - +