From 93b1e81df23da4d9ae3345b3a9767a87f66c2099 Mon Sep 17 00:00:00 2001 From: Richard Cordovano Date: Thu, 29 Nov 2018 09:07:04 -0500 Subject: [PATCH] Initial ImageGalleryTopComponent improvements with TODOs --- .../imagegallery/ImageGalleryController.java | 13 +- .../imagegallery/ImageGalleryModule.java | 421 +++++++------- .../ImageGalleryOptionsPanel.java | 2 +- .../ImageGalleryTopComponent.java | 518 ++++++++++-------- .../imagegallery/actions/OpenAction.java | 55 +- .../datamodel/grouping/DrawableGroup.java | 14 +- .../datamodel/grouping/GroupManager.java | 2 +- .../gui/drawableviews/GroupPane.java | 8 +- 8 files changed, 545 insertions(+), 488 deletions(-) diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java index abe89abf40..4c192f2da5 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2018 Basis Technology Corp. + * Copyright 2013-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -183,7 +183,9 @@ public final class ImageGalleryController { } ImageGalleryController(@Nonnull Case newCase) throws TskCoreException { - + + + this.autopsyCase = Objects.requireNonNull(newCase); this.sleuthKitCase = newCase.getSleuthkitCase(); @@ -318,17 +320,14 @@ public final class ImageGalleryController { /** * reset the state of the controller (eg if the case is closed) */ - public synchronized void reset() { - logger.info("Closing ImageGalleryControler for case."); //NON-NLS - + public synchronized void shutDown() { selectionModel.clearSelection(); thumbnailCache.clearCache(); historyManager.clear(); groupManager.reset(); - shutDownDBExecutor(); drawableDB.close(); - dbExecutor = getNewDBExecutor(); + //dbExecutor = getNewDBExecutor(); RJCTODO } /** diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryModule.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryModule.java index 530e3eb7d6..f2be11ddae 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-18 Basis Technology Corp. + * Copyright 2013-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,12 +22,14 @@ import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.EnumSet; +import java.util.Set; import java.util.logging.Level; import javafx.application.Platform; +import javax.annotation.concurrent.GuardedBy; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import static org.apache.commons.lang3.StringUtils.isNotBlank; -import org.openide.util.Exceptions; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; @@ -35,11 +37,9 @@ 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.DATA_ADDED; import static org.sleuthkit.autopsy.ingest.IngestManager.IngestModuleEvent.FILE_DONE; import org.sleuthkit.autopsy.ingest.ModuleDataEvent; @@ -53,71 +53,100 @@ import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; -/** static definitions, utilities, and listeners for the ImageGallery module */ +/** + * This class is reponsible for handling selected application events for the + * image gallery module, managing the image gallery module's per case MVC + * controller and keeping track of the following state: the module name, the + * module output directory and whether or not the ingest gallery module is + * enabled for the current case. + */ @NbBundle.Messages({"ImageGalleryModule.moduleName=Image Gallery"}) public class ImageGalleryModule { private static final Logger logger = Logger.getLogger(ImageGalleryModule.class.getName()); - private static final String MODULE_NAME = Bundle.ImageGalleryModule_moduleName(); - + private static final Set CASE_EVENTS_OF_INTEREST = EnumSet.of( + Case.Events.CURRENT_CASE, + Case.Events.DATA_SOURCE_ADDED, + Case.Events.CONTENT_TAG_ADDED, + Case.Events.CONTENT_TAG_DELETED + ); private static final Object controllerLock = new Object(); + @GuardedBy("controllerLock") private static ImageGalleryController controller; - public static ImageGalleryController getController() throws TskCoreException, NoCurrentCaseException { + /** + * Gets the per case image gallery controller for the current case. The + * controller is changed in the case event listener. + * + * @return The image gallery controller for the current case. + * + * @throws TskCoreException If there is a problem creating the controller. + */ + public static ImageGalleryController getController() throws TskCoreException { synchronized (controllerLock) { if (controller == null) { - controller = new ImageGalleryController(Case.getCurrentCaseThrows()); + try { + Case currentCase = Case.getCurrentCaseThrows(); + controller = new ImageGalleryController(currentCase); + } catch (NoCurrentCaseException ex) { + throw new TskCoreException("no current case", ex); + } } return controller; } } /** - * - * - * This method is invoked by virtue of the OnStart annotation on the OnStart - * class class + * Sets the implicit exit property attribute of the JavaFX Runtime to false + * and sets up listeners for application events. It is invoked at + * application start up by virtue of the OnStart annotation on the OnStart + * class in this package. */ 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()); + Case.addEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, new CaseEventListener()); } + /** + * Gets the image gallery module name. + * + * @return The module name, + */ static String getModuleName() { return MODULE_NAME; } /** - * get the Path to the Case's ImageGallery ModuleOutput subfolder; ie - * ".../[CaseName]/ModuleOutput/Image Gallery/" + * Gets the path to the image gallery module output folder for a given case. * - * @param theCase the case to get the ImageGallery ModuleOutput subfolder - * for + * @param theCase The case. * - * @return the Path to the ModuleOuput subfolder for Image Gallery + * @return The path to the image gallery module output folder for the case. */ public static Path getModuleOutputDir(Case theCase) { return Paths.get(theCase.getModuleDirectory(), getModuleName()); } - /** provides static utilities, can not be instantiated */ + /** + * Prevents instantiation. + */ private ImageGalleryModule() { } - /** is listening enabled for the given case + /** + * Indicates whether or not the image gallery module is enabled for a given + * case. * - * @param c + * @param theCase The case. * - * @return true if listening is enabled for the given case, false otherwise + * @return True or false. */ - static boolean isEnabledforCase(Case c) { - if (c != null) { - String enabledforCaseProp = new PerCaseProperties(c).getConfigSetting(ImageGalleryModule.MODULE_NAME, PerCaseProperties.ENABLED); + static boolean isEnabledforCase(Case theCase) { + if (theCase != null) { + String enabledforCaseProp = new PerCaseProperties(theCase).getConfigSetting(ImageGalleryModule.MODULE_NAME, PerCaseProperties.ENABLED); return isNotBlank(enabledforCaseProp) ? Boolean.valueOf(enabledforCaseProp) : ImageGalleryPreferences.isEnabledByDefault(); } else { return false; @@ -125,197 +154,184 @@ public class ImageGalleryModule { } /** - * Is the given file 'supported' and not 'known'(nsrl hash hit). If so we - * should include it in {@link DrawableDB} and UI + * Indicates whether or not a given file is of interest to the image gallery + * module (is "drawable") and is not marked as a "known" file (e.g., is not + * a file in the NSRL hash set). * - * @param abstractFile + * @param file The file. * - * @return true if the given {@link AbstractFile} is "drawable" and not - * 'known', else false + * @return True if the file is "drawable" and not "known", false otherwise. * - * @throws - * org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector.FileTypeDetectorInitException + * @throws FileTypeDetectorInitException If there is an error determining + * the type of the file. */ - public static boolean isDrawableAndNotKnown(AbstractFile abstractFile) throws FileTypeDetector.FileTypeDetectorInitException { + private static boolean isDrawableAndNotKnown(AbstractFile abstractFile) throws FileTypeDetector.FileTypeDetectorInitException { return (abstractFile.getKnown() != TskData.FileKnown.KNOWN) && FileTypeUtils.isDrawable(abstractFile); } /** - * Listener for IngestModuleEvents + * A listener for ingest module application events. */ static private class IngestModuleEventListener implements PropertyChangeListener { @Override - public void propertyChange(PropertyChangeEvent evt) { + public void propertyChange(PropertyChangeEvent event) { + /* + * If running in "headless" mode, there is no need to process any + * ingest module events during the current session. + * + * Note that this check cannot be done earlier on start up because + * the "headless" property may not have been set yet. + */ 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; } - /* 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) { + /* + * Only process individual files and artifacts in "real time" on the + * node that is running the ingest job. On a remote node, image + * files are processed as a group when the ingest job is complete. + */ + if (((AutopsyEvent) event).getSourceType() != AutopsyEvent.SourceType.LOCAL) { return; } - // Bail out if the case is closed + ImageGalleryController currentController; try { - if (controller == null || Case.getCurrentCaseThrows() == null) { - return; - } - } catch (NoCurrentCaseException ex) { + currentController = getController(); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, String.format("Failed to handle %s event", event.getPropertyName()), ex); //NON-NLS return; } - if (IngestManager.IngestModuleEvent.valueOf(evt.getPropertyName()) == FILE_DONE) { - - // getOldValue has fileID getNewValue has Abstractfile - AbstractFile file = (AbstractFile) evt.getNewValue(); - if (false == file.isFile()) { - return; - } - - try { - ImageGalleryController con = getController(); - if (con.isListeningEnabled()) { + String eventType = event.getPropertyName(); + switch (IngestManager.IngestModuleEvent.valueOf(eventType)) { + case FILE_DONE: + AbstractFile file = (AbstractFile) event.getNewValue(); + if (!file.isFile()) { + return; + } + if (currentController.isListeningEnabled()) { try { - // Update the entry if it is a picture and not in NSRL if (isDrawableAndNotKnown(file)) { - con.queueDBTask(new ImageGalleryController.UpdateFileTask(file, controller.getDatabase())); - } - // Remove it from the DB if it is no longer relevant, but had the correct extension - 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())); + currentController.queueDBTask(new ImageGalleryController.UpdateFileTask(file, currentController.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."); + logger.log(Level.SEVERE, String.format("Failed to determine if file is of interest to the image gallery module, ignoring file (obj_id=%d)", file.getId()), ex); //NON-NLS } } - } catch (NoCurrentCaseException ex) { - logger.log(Level.SEVERE, "Attempted to access ImageGallery with no case open.", ex); //NON-NLS - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error getting ImageGalleryController.", ex); //NON-NLS - } - } - else if (IngestManager.IngestModuleEvent.valueOf(evt.getPropertyName()) == DATA_ADDED) { - ModuleDataEvent mde = (ModuleDataEvent) evt.getOldValue(); - - if (mde.getBlackboardArtifactType().getTypeID() == ARTIFACT_TYPE.TSK_METADATA_EXIF.getTypeID()) { - DrawableDB drawableDB = controller.getDatabase(); - if (mde.getArtifacts() != null) { - for (BlackboardArtifact art : mde.getArtifacts()) { - drawableDB.addExifCache(art.getObjectID()); + break; + case DATA_ADDED: + ModuleDataEvent artifactAddedEvent = (ModuleDataEvent) event.getOldValue(); + if (artifactAddedEvent.getBlackboardArtifactType().getTypeID() == ARTIFACT_TYPE.TSK_METADATA_EXIF.getTypeID()) { + DrawableDB drawableDB = currentController.getDatabase(); + if (artifactAddedEvent.getArtifacts() != null) { + for (BlackboardArtifact art : artifactAddedEvent.getArtifacts()) { + drawableDB.addExifCache(art.getObjectID()); + } + } + } else if (artifactAddedEvent.getBlackboardArtifactType().getTypeID() == ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID()) { + DrawableDB drawableDB = currentController.getDatabase(); + if (artifactAddedEvent.getArtifacts() != null) { + for (BlackboardArtifact art : artifactAddedEvent.getArtifacts()) { + drawableDB.addHashSetCache(art.getObjectID()); + } } } - } - else if (mde.getBlackboardArtifactType().getTypeID() == ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID()) { - DrawableDB drawableDB = controller.getDatabase(); - if (mde.getArtifacts() != null) { - for (BlackboardArtifact art : mde.getArtifacts()) { - drawableDB.addHashSetCache(art.getObjectID()); - } - } - } + break; + default: + break; } } } /** - * Listener for case events. + * A listener for case application events. */ static private class CaseEventListener implements PropertyChangeListener { @Override - public void propertyChange(PropertyChangeEvent evt) { + public void propertyChange(PropertyChangeEvent event) { + /* + * If running in "headless" mode, there is no need to process any + * case events during the current session. Note that this check + * cannot be done earlier in onStart because the "headless" property + * may not have been set yet. + */ 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; - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error getting ImageGalleryController.", 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); + Case.Events eventType = Case.Events.valueOf(event.getPropertyName()); + if (eventType == Case.Events.CURRENT_CASE) { + synchronized (controllerLock) { + if (event.getNewValue() != null && event.getNewValue() instanceof Case) { + /* + * CURRENT_CASE(_OPENED) event. Construct a new Image + * Gallery controller. + */ + Case newCase = (Case) event.getNewValue(); + try { + controller = new ImageGalleryController(newCase); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Failed to construct controller for new case", ex); + } + } else if (event.getOldValue() != null && event.getOldValue() instanceof Case) { + /* + * CURRENT_CASE(_CLOSED) event. Shut down the controller + * for the case and close the top component, if it is + * open. + */ + SwingUtilities.invokeLater(ImageGalleryTopComponent::closeTopComponent); + controller.shutDown(); + controller = null; + } + } + } else { + ImageGalleryController currentController; + try { + currentController = getController(); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, String.format("Failed to handle %s event", event.getPropertyName()), ex); //NON-NLS + return; + } + + switch (eventType) { + case DATA_SOURCE_ADDED: + if (((AutopsyEvent) event).getSourceType() == AutopsyEvent.SourceType.LOCAL) { + Content newDataSource = (Content) event.getNewValue(); + if (currentController.isListeningEnabled()) { + currentController.getDatabase().insertOrUpdateDataSource(newDataSource.getId(), DrawableDB.DrawableDbBuildStatusEnum.DEFAULT); } } - } - 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()) { - controller.getDatabase().insertOrUpdateDataSource(newDataSource.getId(), DrawableDB.DrawableDbBuildStatusEnum.DEFAULT); + break; + case CONTENT_TAG_ADDED: + final ContentTagAddedEvent tagAddedEvent = (ContentTagAddedEvent) event; + long objId = tagAddedEvent.getAddedTag().getContent().getId(); + DrawableDB drawableDB = currentController.getDatabase(); + drawableDB.addTagCache(objId); + if (drawableDB.isInDB(objId)) { // RJCTODO: Put in cache before in DB check? + currentController.getTagsManager().fireTagAddedEvent(tagAddedEvent); } - } - break; - case CONTENT_TAG_ADDED: - final ContentTagAddedEvent tagAddedEvent = (ContentTagAddedEvent) evt; - - long objId = tagAddedEvent.getAddedTag().getContent().getId(); - - // update the cache - DrawableDB drawableDB = controller.getDatabase(); - drawableDB.addTagCache(objId); - - if (con.getDatabase().isInDB(objId)) { - 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; + break; + case CONTENT_TAG_DELETED: + final ContentTagDeletedEvent tagDeletedEvent = (ContentTagDeletedEvent) event; + if (currentController.getDatabase().isInDB(tagDeletedEvent.getDeletedTagInfo().getContentID())) { + currentController.getTagsManager().fireTagDeletedEvent(tagDeletedEvent); + } + break; + default: + logger.log(Level.SEVERE, String.format("Received %s event with no subscription", event.getPropertyName())); //NON-NLS + break; + } } } } /** - * Listener for Ingest Job events. + * A listener for ingest job application events. */ static private class IngestJobEventListener implements PropertyChangeListener { @@ -326,42 +342,54 @@ public class ImageGalleryModule { "ImageGalleryController.dataSourceAnalyzed.confDlg.title=Image Gallery" }) @Override - public void propertyChange(PropertyChangeEvent evt) { - IngestJobEvent eventType = IngestJobEvent.valueOf(evt.getPropertyName()); - + public void propertyChange(PropertyChangeEvent event) { + ImageGalleryController controller; try { - ImageGalleryController controller = getController(); - - if (eventType == IngestJobEvent.DATA_SOURCE_ANALYSIS_STARTED) { - - if (((AutopsyEvent) evt).getSourceType() == AutopsyEvent.SourceType.LOCAL) { + controller = getController(); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, String.format("Failed to handle %s event", event.getPropertyName()), ex); //NON-NLS + return; + } + + String eventType = event.getPropertyName(); + switch (IngestManager.IngestJobEvent.valueOf(eventType)) { + case DATA_SOURCE_ANALYSIS_STARTED: + if (((AutopsyEvent) event).getSourceType() == AutopsyEvent.SourceType.LOCAL) { if (controller.isListeningEnabled()) { - DataSourceAnalysisStartedEvent dataSourceAnalysisStartedEvent = (DataSourceAnalysisStartedEvent) evt; + DataSourceAnalysisStartedEvent dataSourceAnalysisStartedEvent = (DataSourceAnalysisStartedEvent) event; Content dataSource = dataSourceAnalysisStartedEvent.getDataSource(); - - controller.getDatabase().insertOrUpdateDataSource(dataSource.getId(), DrawableDB.DrawableDbBuildStatusEnum.IN_PROGRESS); + + controller.getDatabase().insertOrUpdateDataSource(dataSource.getId(), DrawableDB.DrawableDbBuildStatusEnum.IN_PROGRESS); } } - } else if (eventType == IngestJobEvent.DATA_SOURCE_ANALYSIS_COMPLETED) { - - if (((AutopsyEvent) evt).getSourceType() == AutopsyEvent.SourceType.LOCAL) { + break; + case DATA_SOURCE_ANALYSIS_COMPLETED: + if (((AutopsyEvent) event).getSourceType() == AutopsyEvent.SourceType.LOCAL) { + /* + * This node just completed analysis of a data source. + * Set the state of the local drawables database. + */ if (controller.isListeningEnabled()) { - DataSourceAnalysisCompletedEvent dataSourceAnalysisCompletedEvent = (DataSourceAnalysisCompletedEvent) evt; + DataSourceAnalysisCompletedEvent dataSourceAnalysisCompletedEvent = (DataSourceAnalysisCompletedEvent) event; Content dataSource = dataSourceAnalysisCompletedEvent.getDataSource(); - - DrawableDB.DrawableDbBuildStatusEnum datasourceDrawableDBStatus = - controller.hasFilesWithNoMimetype(dataSource) ? - DrawableDB.DrawableDbBuildStatusEnum.DEFAULT : - DrawableDB.DrawableDbBuildStatusEnum.COMPLETE; - + try { + DrawableDB.DrawableDbBuildStatusEnum datasourceDrawableDBStatus + = controller.hasFilesWithNoMimetype(dataSource) + ? DrawableDB.DrawableDbBuildStatusEnum.DEFAULT + : DrawableDB.DrawableDbBuildStatusEnum.COMPLETE; controller.getDatabase().insertOrUpdateDataSource(dataSource.getId(), datasourceDrawableDBStatus); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Failed to query case database to determine drawables database state", ex); + } } - return; - } - - if (((AutopsyEvent) evt).getSourceType() == AutopsyEvent.SourceType.REMOTE) { - // 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 + } else { + /* + * A remote node just completed analysis of a data + * source. The local drawables database is therefore + * stale. If the image gallery top component is open, + * give the user an opportunity to update the drawables + * database now. + */ controller.setStale(true); if (controller.isListeningEnabled()) { SwingUtilities.invokeLater(() -> { @@ -384,12 +412,9 @@ public class ImageGalleryModule { }); } } - } - } - catch (NoCurrentCaseException ex) { - logger.log(Level.SEVERE, "Attempted to access ImageGallery with no case open.", ex); //NON-NLS - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error getting ImageGalleryController.", ex); //NON-NLS + break; + default: + break; } } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryOptionsPanel.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryOptionsPanel.java index df0dacd3fe..92e65e2f4d 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryOptionsPanel.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryOptionsPanel.java @@ -212,7 +212,7 @@ final class ImageGalleryOptionsPanel extends javax.swing.JPanel { } catch (NoCurrentCaseException ex) { // It's not an error if there's no case open } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error getting ImageGalleryController.", ex); //NON-NLS + logger.log(Level.SEVERE, "Failed to get image gallery controller", ex); //NON-NLS } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryTopComponent.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryTopComponent.java index 927dc6b70e..2a5f7c12d4 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryTopComponent.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryTopComponent.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2018 Basis Technology Corp. + * Copyright 2015-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -49,6 +49,7 @@ import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.stage.Modality; +import javax.annotation.concurrent.GuardedBy; import javax.swing.SwingUtilities; import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; import static org.apache.commons.lang3.ObjectUtils.notEqual; @@ -61,7 +62,6 @@ 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; @@ -81,16 +81,11 @@ import org.sleuthkit.datamodel.DataSource; import org.sleuthkit.datamodel.TskCoreException; /** - * Top component which displays ImageGallery interface. - * - * Although ImageGallery doesn't currently use the explorer manager, this - * TopComponent provides one through the getExplorerManager method. However, - * this does not seem to function correctly unless a Netbeans provided explorer - * view is present in the TopComponenet, even if it is invisible/ zero sized + * The singleton Image Gallery top component. */ @TopComponent.Description( preferredID = "ImageGalleryTopComponent", - //iconBase = "org/sleuthkit/autopsy/imagegallery/images/lightbulb.png" use this to put icon in window title area, + //iconBase = "org/sleuthkit/autopsy/imagegallery/images/lightbulb.png", /*use this to put icon in window title area*/ persistenceType = TopComponent.PERSISTENCE_NEVER) @RetainLocation("ImageGallery") @TopComponent.Registration(mode = "ImageGallery", openAtStartup = false) @@ -101,13 +96,15 @@ import org.sleuthkit.datamodel.TskCoreException; @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives public final class ImageGalleryTopComponent extends TopComponent implements ExplorerManager.Provider, Lookup.Provider { - public final static String PREFERRED_ID = "ImageGalleryTopComponent"; // NON-NLS + private static final long serialVersionUID = 1L; + public final static String PREFERRED_ID = "ImageGalleryTopComponent"; // NON-NLS // RJCTODO: This should not be public, clients should call getTopComponent instead 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 Object controllerLock = new Object(); + @GuardedBy("controllerLock") private ImageGalleryController controller; private SplitPane splitPane; @@ -125,148 +122,86 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl private final Region infoOverLayBackground = new TranslucentRegion(); /** - * Returns whether the ImageGallery window is open or not. + * Queries whether the singleton Image Gallery top component's window is + * open. Note that calling this method will cause the top component to be + * constructed if it does not already exist. * - * @return true, if Image gallery is opened, false otherwise + * @return True or false. */ public static boolean isImageGalleryOpen() { - - final TopComponent topComponent = WindowManager.getDefault().findTopComponent(PREFERRED_ID); - if (topComponent != null) { - return topComponent.isOpened(); - } - return false; + return getTopComponent().isOpened(); } /** - * Returns the top component window. + * Gets the singleton Image Gallery top component. Note that calling this + * method will cause the top component to be constructed if it does not + * already exist. * - * @return Image gallery top component window, null if it's not open + * @return The top component. */ - public static TopComponent getTopComponent() { - return WindowManager.getDefault().findTopComponent(PREFERRED_ID); + public static ImageGalleryTopComponent getTopComponent() { + return (ImageGalleryTopComponent) WindowManager.getDefault().findTopComponent(PREFERRED_ID); } /** - * Open the ImageGalleryTopComponent. + * Creates the Image Gallery top component if it does not already exist and + * opens its window. * - * @throws NoCurrentCaseException If there is no case open. - * @throws TskCoreException If there is a problem accessing the case - * db. + * @throws TskCoreException If there is a problem opening the top component. */ @ThreadConfined(type = ThreadConfined.ThreadType.AWT) - @Messages({ - "ImageGalleryTopComponent.chooseDataSourceDialog.headerText=Choose a data source to view.", - "ImageGalleryTopComponent.chooseDataSourceDialog.contentText=Data source:", - "ImageGalleryTopComponent.chooseDataSourceDialog.all=All", - "ImageGalleryTopComponent.chooseDataSourceDialog.titleText=Image Gallery",}) - public static void openTopComponent() throws NoCurrentCaseException, TskCoreException { - - // This creates the top component and adds the UI widgets (via the constructor) if it has not yet been opened - final TopComponent topComponent = WindowManager.getDefault().findTopComponent(PREFERRED_ID); - if (topComponent == null) { - return; - } - + public static void openTopComponent() throws TskCoreException { + final ImageGalleryTopComponent topComponent = getTopComponent(); if (topComponent.isOpened()) { - showTopComponent(topComponent); - return; + showTopComponent(); + } else { + topComponent.getCurrentControllerAndOpen(); } - - // Wait until the FX UI has been created. This way, we can always - // show the gray progress screen - // TODO: do this in a more elegant way. - while (topComponentInitialized == false) { - } - - ImageGalleryController controller = ImageGalleryModule.getController(); - ImageGalleryTopComponent igTopComponent = (ImageGalleryTopComponent) topComponent; - igTopComponent.setController(controller); - - //gather information about datasources and the groupmanager in a bg thread. - new Thread(new Task() { - @Override - protected Void call() throws Exception { - Map dataSourcesTooManyFiles = new HashMap<>(); - List dataSources = controller.getSleuthKitCase().getDataSources(); - - /* - * If there is only one datasource or the grouping is already - * set to something other than path , don't bother to ask for - * datasource. - */ - if (dataSources.size() <= 1 - || controller.getGroupManager().getGroupBy() != DrawableAttribute.PATH) { - // null represents all datasources, which is only one in this case. - dataSourcesTooManyFiles.put(null, controller.hasTooManyFiles(null)); - igTopComponent.showDataSource(null, dataSourcesTooManyFiles); - return null; - } - - /* - * Else there is more than one data source and the grouping is - * PATH (the default): open a dialog prompting the user to pick - * a datasource. - */ - dataSources.add(0, null); //null represents all datasources - //first, while still on background thread, gather viewability info for the datasources. - for (DataSource dataSource : dataSources) { - dataSourcesTooManyFiles.put(dataSource, controller.hasTooManyFiles(dataSource)); - } - - Platform.runLater(() -> { - //configure the dialog - List> dataSourceOptionals = dataSources.stream().map(Optional::ofNullable).collect(Collectors.toList()); - ChoiceDialog> datasourceDialog = new ChoiceDialog<>(null, dataSourceOptionals); - datasourceDialog.setTitle(Bundle.ImageGalleryTopComponent_chooseDataSourceDialog_titleText()); - datasourceDialog.setHeaderText(Bundle.ImageGalleryTopComponent_chooseDataSourceDialog_headerText()); - datasourceDialog.setContentText(Bundle.ImageGalleryTopComponent_chooseDataSourceDialog_contentText()); - datasourceDialog.initModality(Modality.APPLICATION_MODAL); - GuiUtils.setDialogIcons(datasourceDialog); - //get the combobox by its css class... this is hacky but should be safe. - @SuppressWarnings(value = "unchecked") - ComboBox> comboBox = (ComboBox>) datasourceDialog.getDialogPane().lookup(".combo-box"); - //set custom cell renderer - comboBox.setCellFactory((ListView> param) -> new DataSourceCell(dataSourcesTooManyFiles, controller.getAllDataSourcesDrawableDBStatus())); - comboBox.setButtonCell(new DataSourceCell(dataSourcesTooManyFiles, controller.getAllDataSourcesDrawableDBStatus())); - - DataSource dataSource = datasourceDialog.showAndWait().orElse(Optional.empty()).orElse(null); - try { - - igTopComponent.showDataSource(dataSource, dataSourcesTooManyFiles); - } catch (TskCoreException ex) { - if (dataSource != null) { - logger.log(Level.SEVERE, "Error showing data source " + dataSource.getName() + ":" + dataSource.getId() + " in Image Gallery", ex); - } else { - logger.log(Level.SEVERE, "Error showing all data sources in Image Gallery.", ex); - } - } - }); - - return null; - } - }).start(); } - synchronized private void showDataSource(DataSource datasource, Map dataSourcesTooManyFiles) throws TskCoreException { - if (dataSourcesTooManyFiles.get(datasource)) { + /** + * Configures the groups manager for the selected data source(s) and opens + * this top component's window. + * + * @param selectedDataSource The data source selected, null if all data + * sources are selected. + * @param dataSourcesTooManyFiles A map of data sources to flags indicating + * whether or not the data source has to many + * files to actually be displayed. + */ + private void openWithSelectedDataSources(DataSource selectedDataSource, Map dataSourcesTooManyFiles) { + if (dataSourcesTooManyFiles.get(selectedDataSource)) { Platform.runLater(ImageGalleryTopComponent::showTooManyFiles); - return; - } - // Display the UI so that they can see the progress screen - SwingUtilities.invokeLater(() -> showTopComponent(this)); - GroupManager groupManager = controller.getGroupManager(); - synchronized (groupManager) { - groupManager.regroup(datasource, groupManager.getGroupBy(), groupManager.getSortBy(), groupManager.getSortOrder(), true); + } else { + /* + * Open the top component's window before configuring the groups + * manager so that the wait cursor animation over the empty, gray + * window will be displayed if the operations takes awhile. + */ + SwingUtilities.invokeLater(() -> showTopComponent()); + synchronized (controllerLock) { + GroupManager groupManager = controller.getGroupManager(); + // RJCTODO: Why are there potentially hazardous nested synchronized + // blocks here (note: method used ot be synchronized)? Why is + // the groups manager not taking responsibility for its own thread + // safety policy? + synchronized (groupManager) { + groupManager.regroup(selectedDataSource, groupManager.getGroupBy(), groupManager.getSortBy(), groupManager.getSortOrder(), true); + } + } } } + /** + * Displays a dialog box informing the user that the data source(s) selected + * to have their images displayed have too many image files and will not be + * displayed. + */ @NbBundle.Messages({"ImageGallery.dialogTitle=Image Gallery", "ImageGallery.showTooManyFiles.contentText=There are too many files in the selected datasource(s) to ensure reasonable performance.", "ImageGallery.showTooManyFiles.headerText="}) - public static void showTooManyFiles() { - Alert dialog = new Alert(Alert.AlertType.INFORMATION, - Bundle.ImageGallery_showTooManyFiles_contentText(), ButtonType.OK); + private static void showTooManyFiles() { + Alert dialog = new Alert(Alert.AlertType.INFORMATION, Bundle.ImageGallery_showTooManyFiles_contentText(), ButtonType.OK); dialog.initModality(Modality.APPLICATION_MODAL); dialog.setTitle(Bundle.ImageGallery_dialogTitle()); GuiUtils.setDialogIcons(dialog); @@ -274,8 +209,14 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl dialog.showAndWait(); } + /** + * Opens the singleton top component's window, brings it to the front and + * gives it focus. Note that calling this method will cause the top + * component to be constructed if it does not already exist. + */ @ThreadConfined(type = ThreadConfined.ThreadType.AWT) - public static void showTopComponent(TopComponent topComponent) { + private static void showTopComponent() { + final ImageGalleryTopComponent topComponent = getTopComponent(); if (topComponent.isOpened() == false) { topComponent.open(); } @@ -283,76 +224,170 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl topComponent.requestActive(); } + /* + * Closes the singleton Image Gallery top component. Note that calling this + * method will cause the top component to be constructed if it does not + * already exist. + */ public static void closeTopComponent() { - if (topComponentInitialized) { - final TopComponent etc = WindowManager.getDefault().findTopComponent(PREFERRED_ID); - if (etc != null) { - try { - etc.close(); - } catch (Exception e) { - logger.log(Level.SEVERE, "failed to close " + PREFERRED_ID, e); // NON-NLS - } - } - } + getTopComponent().close(); } + /** + * Contructs the singleton Image Gallery top component. Called by the + * NetBeans WindowManager. + */ public ImageGalleryTopComponent() { setName(Bundle.CTL_ImageGalleryTopComponent()); initComponents(); - try { - setController(ImageGalleryModule.getController()); - } catch (NoCurrentCaseException ex) { - logger.log(Level.SEVERE, "Attempted to access ImageGallery with no case open.", ex); //NON-NLS - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error getting ImageGalleryController.", ex); //NON-NLS - } } - synchronized private void setController(ImageGalleryController controller) { - if (notEqual(this.controller, controller)) { - if (this.controller != null) { - 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); + /** + * Gets the current controller, allows the user to select the data sources + * for which images are to be displayed and opens the top component's + * window. + * + * @throws TskCoreException If there is an error getting the current + * controller. + */ + @Messages({ + "ImageGalleryTopComponent.chooseDataSourceDialog.headerText=Choose a data source to view.", + "ImageGalleryTopComponent.chooseDataSourceDialog.contentText=Data source:", + "ImageGalleryTopComponent.chooseDataSourceDialog.all=All", + "ImageGalleryTopComponent.chooseDataSourceDialog.titleText=Image Gallery",}) + private void getCurrentControllerAndOpen() throws TskCoreException { + ImageGalleryController currentController = ImageGalleryModule.getController(); + /* + * First, dispatch a task to run in the JavaFX thread. This task will + * swap the new controller, if there is one, into this top component and + * its child UI components. It also queues another JavaFX thread task to + * check for analyzed groups, which has the side effect of managing the + * spinner(s) that take the place of a wait cursor. + */ + Platform.runLater(new Runnable() { + @Override + public void run() { + synchronized (controllerLock) { + if (notEqual(controller, currentController)) { + controller = currentController; + /* + * Create or re-create the top component's child UI + * components. This is currently done every time a new + * controller is created (i.e., a new case is opened). + * It could be done by resetting the controller in the + * child UI components instead. + */ + fullUIStack = new StackPane(); + myScene = new Scene(fullUIStack); + jfxPanel.setScene(myScene); + groupPane = new GroupPane(controller); + centralStack = new StackPane(groupPane); + 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); - controller.regroupDisabledProperty().addListener((Observable observable) -> checkForGroups()); - controller.getGroupManager().getAnalyzedGroups().addListener((Observable observable) -> Platform.runLater(() -> checkForGroups())); + /* + * Set up for a call to checkForGroups to happen + * whenever the controller's regrouping disabled + * property or the group manager's analyzed groups + * property changes. + */ + // RJCTODO: Why was the first lambda not set up to happen on + // the JavaFX thread? I am using that thread confinement and + // a volatile controller reference for simplified thread safety, + // is there a problem with this? It seems like this is a bug, + // since why would we want this code to execute both in the + // JavaFX thread and elsewhere? + controller.regroupDisabledProperty().addListener((Observable unused) -> Platform.runLater(() -> checkForAnalyzedGroups())); + controller.getGroupManager().getAnalyzedGroups().addListener((Observable unused) -> Platform.runLater(() -> checkForAnalyzedGroups())); - topComponentInitialized = true; + /* + * Dispatch a later task to call check for groups. Note + * that this method displays one or more spinner(s) that + * take the place of a wait cursor if there are no + * analyzed groups yet, ingest is running, etc. + */ + // RJCTODO: Is there a race condition here, since this task will be + // executed before the task to actually open the top component window? + // It seems like this might be a sort of a hack and I am wondering + // why this can't be done in openWithSelectedDataSources instead. + Platform.runLater(() -> checkForAnalyzedGroups()); + } - // This will cause the UI to show the progress dialog - Platform.runLater(() -> checkForGroups()); + /* + * Kick off a background task to query the case database for + * data sources. This task may queue another task for the + * JavaFX thread to allow the user to select which data + * sources for which to display images. Ultimately, a task + * will be queued for the AWT EDT that will show the top + * component window. + */ + new Thread(new Task() { + @Override + protected Void call() throws Exception { + synchronized (controllerLock) { + /* + * If there is only one datasource or the + * grouping criterion is already set to + * something other than by path (the default), + * proceed to open this top component. + * Otherwise, do a dialog to allow the user to + * select the data sources for which images are + * to be displayed, then open the top component. + */ + List dataSources = controller.getSleuthKitCase().getDataSources(); + Map dataSourcesWithTooManyFiles = new HashMap<>(); + // RJCTODO: At least some of this designation of "all data sources" with null seems uneccessary; + // in any case, the use of nulls and zeros here is + // very confusing and should be reworked. Why was this done? + if (dataSources.size() <= 1 + || controller.getGroupManager().getGroupBy() != DrawableAttribute.PATH) { + dataSourcesWithTooManyFiles.put(null, controller.hasTooManyFiles(null)); + openWithSelectedDataSources(null, dataSourcesWithTooManyFiles); + } else { + dataSources.add(0, null); + for (DataSource dataSource : dataSources) { + dataSourcesWithTooManyFiles.put(dataSource, controller.hasTooManyFiles(dataSource)); + } + Platform.runLater(() -> { + List> dataSourceOptionals = dataSources.stream().map(Optional::ofNullable).collect(Collectors.toList()); + ChoiceDialog> datasourceDialog = new ChoiceDialog<>(null, dataSourceOptionals); + datasourceDialog.setTitle(Bundle.ImageGalleryTopComponent_chooseDataSourceDialog_titleText()); + datasourceDialog.setHeaderText(Bundle.ImageGalleryTopComponent_chooseDataSourceDialog_headerText()); + datasourceDialog.setContentText(Bundle.ImageGalleryTopComponent_chooseDataSourceDialog_contentText()); + datasourceDialog.initModality(Modality.APPLICATION_MODAL); + GuiUtils.setDialogIcons(datasourceDialog); + @SuppressWarnings(value = "unchecked") + ComboBox> comboBox = (ComboBox>) datasourceDialog.getDialogPane().lookup(".combo-box"); + comboBox.setCellFactory((ListView> param) -> new DataSourceCell(dataSourcesWithTooManyFiles, controller.getAllDataSourcesDrawableDBStatus())); + comboBox.setButtonCell(new DataSourceCell(dataSourcesWithTooManyFiles, controller.getAllDataSourcesDrawableDBStatus())); + DataSource dataSource = datasourceDialog.showAndWait().orElse(Optional.empty()).orElse(null); + openWithSelectedDataSources(dataSource, dataSourcesWithTooManyFiles); + }); + } + return null; + } + } + }).start(); } - }); - } + } + }); } /** @@ -400,6 +435,14 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl @Override public ExplorerManager getExplorerManager() { + /* + * Although ImageGallery doesn't currently use the explorer manager, + * this TopComponent provides one through the getExplorerManager method. + * However, this does not seem to function correctly unless a Netbeans + * provided explorer view is present in the TopComponenet, even if it is + * invisible/ zero sized + */ + // RJCTODO: Why is this here? return em; } @@ -409,11 +452,10 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl } /** - * 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. - * - * This gets called when any group becomes analyzed and when started. + * Checks if there are any fully analyzed groups available from the groups + * manager and removes the blocking progress spinner if there are analyzed + * groups; otherwise adds a blocking progress spinner with an appropriate + * message. */ @ThreadConfined(type = ThreadConfined.ThreadType.JFX) @NbBundle.Messages({ @@ -427,57 +469,59 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl "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(); + private void checkForAnalyzedGroups() { + synchronized (controllerLock) { + GroupManager groupManager = controller.getGroupManager(); - // if there are groups to display, then display them - // @@@ Need to check timing on this and make sure we have only groups for the selected DS. Seems like rebuild can cause groups to be created for a DS that is not later selected... - if (isNotEmpty(groupManager.getAnalyzedGroups())) { - clearNotification(); - return; - } - - // display a message based on if ingest is running and/or listening - 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())); + // if there are groups to display, then display them + // @@@ Need to check timing on this and make sure we have only groups for the selected DS. Seems like rebuild can cause groups to be created for a DS that is not later selected... + if (isNotEmpty(groupManager.getAnalyzedGroups())) { + clearNotification(); + return; } - return; - } - // display a message about stuff still being in the queue - if (controller.getDBTasksQueueSizeProperty().get() > 0) { - replaceNotification(fullUIStack, - new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg3(), - new ProgressIndicator())); - return; - } - - // are there are files in the DB? - try { - if (controller.getDatabase().countAllFiles() <= 0) { - // there are no files in db + // display a message based on if ingest is running and/or listening + if (IngestManager.getInstance().isIngestRunning()) { if (controller.isListeningEnabled()) { - replaceNotification(fullUIStack, - new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg5())); + replaceNotification(centralStack, + new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg2(), + new ProgressIndicator())); } else { replaceNotification(fullUIStack, - new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg4())); + new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg1())); } 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())); + // display a message about stuff still being in the queue + if (controller.getDBTasksQueueSizeProperty().get() > 0) { + replaceNotification(fullUIStack, + new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg3(), + new ProgressIndicator())); + return; + } + + // are there are files in the DB? + 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())); + } } } @@ -491,17 +535,17 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl } + /** + * Removes the spinner(s). + */ @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 + * A partially opaque region used to block out parts of the UI behind a * pseudo dialog. */ static final private class TranslucentRegion extends Region { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java index 5a921e6130..429051b931 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2015-2018 Basis Technology Corp. + * Copyright 2013-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -70,7 +70,7 @@ import org.sleuthkit.datamodel.TskCoreException; + "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.notAnalyzedDlg.msg=No image/video files available to display yet.\n" - + "Please run FileType and EXIF ingest modules.", + + "Please run FileType and EXIF ingest modules.", "OpenAction.stale.confDlg.title=Image Gallery"}) public final class OpenAction extends CallableSystemAction { @@ -147,19 +147,20 @@ public final class OpenAction extends CallableSystemAction { try { currentCase = Case.getCurrentCaseThrows(); } catch (NoCurrentCaseException ex) { - logger.log(Level.SEVERE, "Exception while getting open case.", ex); - return; - } - ImageGalleryController controller; - try { - controller = ImageGalleryModule.getController(); - } catch (TskCoreException | NoCurrentCaseException ex) { - logger.log(Level.SEVERE, "Exception while getting ImageGalleryController for current case.", ex); + logger.log(Level.SEVERE, "No current case", ex); return; } Platform.runLater(() -> { + ImageGalleryController controller; + try { + controller = ImageGalleryModule.getController(); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Failed to get ImageGalleryController", ex); + return; + } + if (currentCase.getCaseType() == Case.CaseType.MULTI_USER_CASE - && ImageGalleryPreferences.isMultiUserCaseInfoDialogDisabled() == false) { + && ImageGalleryPreferences.isMultiUserCaseInfoDialogDisabled() == false) { Alert dialog = new Alert(Alert.AlertType.INFORMATION); dialog.initModality(Modality.APPLICATION_MODAL); dialog.setResizable(true); @@ -185,32 +186,32 @@ public final class OpenAction extends CallableSystemAction { } private void checkDBStale(ImageGalleryController controller) { - - ListenableFuture> dataSourceStatusMapFuture = TaskUtils.getExecutorForClass(OpenAction.class) + + ListenableFuture> dataSourceStatusMapFuture = TaskUtils.getExecutorForClass(OpenAction.class) .submit(controller::getAllDataSourcesDrawableDBStatus); - + addFXCallback(dataSourceStatusMapFuture, dataSourceStatusMap -> { - + boolean dbIsStale = false; for (Map.Entry entry : dataSourceStatusMap.entrySet()) { DrawableDbBuildStatusEnum status = entry.getValue(); if (DrawableDbBuildStatusEnum.COMPLETE != status) { - dbIsStale = true; + dbIsStale = true; } - } - + } + //back on fx thread. if (false == dbIsStale) { //drawable db is not stale, just open it openTopComponent(); } else { - + // If there is only one datasource and it's in DEFAULT State - // ingest modules need to be run on the data source - if (dataSourceStatusMap.size()== 1) { + if (dataSourceStatusMap.size() == 1) { Map.Entry entry = dataSourceStatusMap.entrySet().iterator().next(); - if (entry.getValue() == DrawableDbBuildStatusEnum.DEFAULT ) { + if (entry.getValue() == DrawableDbBuildStatusEnum.DEFAULT) { Alert alert = new Alert(Alert.AlertType.WARNING, Bundle.OpenAction_notAnalyzedDlg_msg(), ButtonType.OK); alert.setTitle(Bundle.OpenAction_stale_confDlg_title()); alert.initModality(Modality.APPLICATION_MODAL); @@ -218,8 +219,8 @@ public final class OpenAction extends CallableSystemAction { alert.showAndWait(); return; } - } - + } + //drawable db is stale, //ask what to do Alert alert = new Alert(Alert.AlertType.WARNING, @@ -235,7 +236,8 @@ public final class OpenAction extends CallableSystemAction { openTopComponent(); } else if (answer == ButtonType.YES) { if (controller.getAutopsyCase().getCaseType() == Case.CaseType.SINGLE_USER_CASE) { - /* For a single-user case, we favor user + /* + * For a single-user case, we favor user * experience, and rebuild the database as soon * as Image Gallery is enabled for the case. * @@ -267,10 +269,9 @@ public final class OpenAction extends CallableSystemAction { SwingUtilities.invokeLater(() -> { try { ImageGalleryTopComponent.openTopComponent(); - } catch (NoCurrentCaseException ex) { - logger.log(Level.SEVERE, "Attempted to access ImageGallery with no case open.", ex);//NON-NLS } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error getting ImageGalleryController.", ex); //NON-NLS} + logger.log(Level.SEVERE, "Failed to open Image Gallery top component", ex); //NON-NLS} + // RJCTODO: Give the user some feedback here } }); } 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 5469afe4d3..98808a4197 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-18 Basis Technology Corp. + * Copyright 2013-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -31,9 +31,7 @@ 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.ImageGalleryModule; import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager; @@ -120,10 +118,8 @@ public class DrawableGroup implements Comparable { .map(ImageGalleryModule.getController().getHashSetManager()::isInAnyHashSet) .filter(Boolean::booleanValue) .count()); - } catch (NoCurrentCaseException ex) { - logger.log(Level.WARNING, "Could not access case during getFilesWithHashSetHitsCount()"); //NON-NLS } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error getting ImageGalleryController.", ex); //NON-NLS + logger.log(Level.SEVERE, "Failed to get image gallery controller", ex); //NON-NLS } } return hashSetHitsCount.get(); @@ -138,12 +134,10 @@ public class DrawableGroup implements Comparable { if (uncatCount.get() < 0) { try { uncatCount.set(ImageGalleryModule.getController().getDatabase().getUncategorizedCount(fileIDs)); - - } catch (TskCoreException | NoCurrentCaseException ex) { - logger.log(Level.WARNING, "Could not access case during getFilesWithHashSetHitsCount()"); //NON-NLS + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Failed to get image gallery controller", ex); //NON-NLS } } - return uncatCount.get(); } 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 99069b7790..b35bae7625 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-18 Basis Technology Corp. + * Copyright 2013-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java index 26a7772759..3d20fb7edb 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-18 Basis Technology Corp. + * Copyright 2013-2018 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,9 +20,7 @@ package org.sleuthkit.autopsy.imagegallery.gui.drawableviews; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Lists; import static com.google.common.collect.Lists.transform; -import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import java.util.ArrayList; import java.util.Arrays; @@ -35,7 +33,6 @@ import java.util.Map; import static java.util.Objects.isNull; import static java.util.Objects.nonNull; import java.util.Optional; -import java.util.function.Consumer; import java.util.logging.Level; import java.util.stream.IntStream; import javafx.animation.Interpolator; @@ -135,18 +132,15 @@ import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewMode; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState; -import org.sleuthkit.autopsy.imagegallery.gui.GuiUtils; import static org.sleuthkit.autopsy.imagegallery.gui.GuiUtils.createAutoAssigningMenuItem; import org.sleuthkit.autopsy.imagegallery.utils.TaskUtils; import static org.sleuthkit.autopsy.imagegallery.utils.TaskUtils.addFXCallback; -import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.TskCoreException; /** * A GroupPane displays the contents of a DrawableGroup. It supports both * GridView and SlideShowView modes by swapping out its internal components. * - * * TODO: Extract the The GridView instance to a separate class analogous to the * SlideShow. *