/* * Autopsy Forensic Browser * * Copyright 2015-2019 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; 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.EnumSet; import java.util.HashMap; 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.ReadOnlyIntegerProperty; import javafx.beans.property.ReadOnlyIntegerWrapper; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleDoubleProperty; import javax.annotation.Nonnull; import javax.annotation.concurrent.GuardedBy; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import org.apache.commons.collections4.CollectionUtils; import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; 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.coreutils.History; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.events.AutopsyEvent; 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.ingest.IngestManager; import org.sleuthkit.autopsy.ingest.ModuleDataEvent; import org.sleuthkit.autopsy.ingest.events.DataSourceAnalysisEvent; import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.DataSource; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; /** * Instances of this class are responsible for fulfilling the controller role in * an MVC pattern implementation where the model is the drawables database for a * case plus the image gallery tables in the case database, and the view is the * image gallery top component. */ public final class ImageGalleryController { private static final Logger logger = Logger.getLogger(ImageGalleryController.class.getName()); private static final Set INGEST_JOB_EVENTS_OF_INTEREST = EnumSet.of(IngestManager.IngestJobEvent.DATA_SOURCE_ANALYSIS_STARTED, IngestManager.IngestJobEvent.DATA_SOURCE_ANALYSIS_COMPLETED); private static final Set INGEST_MODULE_EVENTS_OF_INTEREST = EnumSet.of(IngestManager.IngestModuleEvent.DATA_ADDED, IngestManager.IngestModuleEvent.FILE_DONE); /* * The file limit for image gallery. If the selected data source (or all * data sources, if that option is selected) has more than this many files * in the tsk_files table, the user cannot use the image gallery. */ private static final long FILE_LIMIT = 6_000_000; 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 ); /* * There is an image gallery controller per case. It is created during the * opening of case resources and destroyed during the closing of case * resources. */ private static final Object controllersByCaseLock = new Object(); @GuardedBy("controllersByCaseLock") private static final Map controllersByCase = new HashMap<>(); /** * A flag that controls whether or not the image gallery controller is * handling various application events. Set to true by default. */ private final SimpleBooleanProperty listeningEnabled; @ThreadConfined(type = ThreadConfined.ThreadType.JFX) private final ReadOnlyBooleanWrapper modelStale; private final ReadOnlyBooleanWrapper metaDataCollapsed; private final SimpleDoubleProperty thumbnailSizeProp; private final ReadOnlyBooleanWrapper regroupDisabled; private final ReadOnlyIntegerWrapper dbTaskQueueSize; private final History historyManager; private final UndoRedoManager undoManager; private final Case theCase; private final SleuthkitCase caseDb; private final CaseEventListener caseEventListener; private final IngestJobEventListener ingestJobEventListener; private final IngestModuleEventListener ingestModuleEventListener; private volatile ImageGalleryTopComponent topComponent; private FileIDSelectionModel selectionModel; private ThumbnailCache thumbnailCache; private DrawableDB drawableDB; private GroupManager groupManager; private HashSetManager hashSetManager; private CategoryManager categoryManager; private DrawableTagsManager tagsManager; private ListeningExecutorService dbExecutor; /** * Creates an image gallery controller for a case. The controller will * create/open the model for the case: a local drawables database and the * image gallery tables in the case database. * * @param theCase The case. * * @throws TskCoreException If there is an issue creating/opening a local * drawables database for the case or the image * gallery tables in the case database. */ static void createController(Case theCase) throws TskCoreException { synchronized (controllersByCaseLock) { if (!controllersByCase.containsKey(theCase.getName())) { ImageGalleryController controller = new ImageGalleryController(theCase); controller.startUp(); controllersByCase.put(theCase.getName(), controller); } } } /** * Gets the image gallery controller for a case. * * @param theCase The case. * * @return The controller or null if it does not exist. */ public static ImageGalleryController getController(Case theCase) { synchronized (controllersByCaseLock) { return controllersByCase.get(theCase.getName()); } } /** * Shuts down the image gallery controller for a case. The controller closes * the model for the case: a local drawables database and the image gallery * tables in the case database. * * @param theCase The case. */ static void shutDownController(Case theCase) { synchronized (controllersByCaseLock) { if (controllersByCase.containsKey(theCase.getName())) { ImageGalleryController controller = controllersByCase.remove(theCase.getName()); controller.shutDown(); } } } /** * Constructs an object that is responsible for fulfilling the controller * role in an MVC pattern implementation where the model is the drawables * database for a case plus the image gallery tables in the case database, * and the view is the image gallery top component. * * @param theCase The case. * * @throws TskCoreException If there is an error constructing the * controller. */ ImageGalleryController(@Nonnull Case theCase) throws TskCoreException { this.theCase = Objects.requireNonNull(theCase); caseDb = theCase.getSleuthkitCase(); listeningEnabled = new SimpleBooleanProperty(false); modelStale = new ReadOnlyBooleanWrapper(false); metaDataCollapsed = new ReadOnlyBooleanWrapper(false); thumbnailSizeProp = new SimpleDoubleProperty(100); regroupDisabled = new ReadOnlyBooleanWrapper(false); dbTaskQueueSize = new ReadOnlyIntegerWrapper(0); historyManager = new History<>(); undoManager = new UndoRedoManager(); setListeningEnabled(ImageGalleryModule.isEnabledforCase(theCase)); caseEventListener = new CaseEventListener(); ingestJobEventListener = new IngestJobEventListener(); ingestModuleEventListener = new IngestModuleEventListener(); } void startUp() throws TskCoreException { selectionModel = new FileIDSelectionModel(this); thumbnailCache = new ThumbnailCache(this); /* * TODO (JIRA-5212): The next two lines need to be executed in this * order. Why? This suggests there is some inappropriate coupling * between the DrawableDB and GroupManager classes. */ groupManager = new GroupManager(this); drawableDB = DrawableDB.getDrawableDB(this); categoryManager = new CategoryManager(this); tagsManager = new DrawableTagsManager(this); tagsManager.registerListener(groupManager); tagsManager.registerListener(categoryManager); hashSetManager = new HashSetManager(drawableDB); setModelIsStale(isDataSourcesTableStale()); dbExecutor = getNewDBExecutor(); listeningEnabled.addListener((observable, wasPreviouslyEnabled, isEnabled) -> { try { /* * For multi-user cases, this listener does nothing because * rebuilding the drawables database is deferred until the Image * Gallery tool is opened. */ if (isEnabled && !wasPreviouslyEnabled && (Case.getCurrentCaseThrows().getCaseType() == CaseType.SINGLE_USER_CASE) && isDataSourcesTableStale()) { rebuildDrawablesDb(); } } catch (NoCurrentCaseException ex) { logger.log(Level.WARNING, "Exception while getting open case.", ex); } }); viewStateProperty().addListener((Observable observable) -> { selectionModel.clearSelection(); undoManager.clear(); }); /* * Disable regrouping when drawables database tasks are enqueued. */ dbTaskQueueSize.addListener(obs -> this.updateRegroupDisabled()); Case.addEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, caseEventListener); IngestManager.getInstance().addIngestJobEventListener(INGEST_JOB_EVENTS_OF_INTEREST, ingestJobEventListener); IngestManager.getInstance().addIngestModuleEventListener(INGEST_MODULE_EVENTS_OF_INTEREST, ingestModuleEventListener); SwingUtilities.invokeLater(() -> { topComponent = ImageGalleryTopComponent.getTopComponent(); }); } /** * Shuts down this image gallery controller. */ public synchronized void shutDown() { logger.log(Level.INFO, String.format("Shutting down image gallery controller for case %s (%s)", theCase.getDisplayName(), theCase.getName())); Case.removeEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, caseEventListener); IngestManager.getInstance().removeIngestJobEventListener(ingestJobEventListener); IngestManager.getInstance().removeIngestModuleEventListener(ingestModuleEventListener); selectionModel.clearSelection(); thumbnailCache.clearCache(); historyManager.clear(); groupManager.reset(); shutDownDBExecutor(); drawableDB.close(); logger.log(Level.INFO, String.format("Completed shut down of image gallery controller for case %s (%s)", theCase.getDisplayName(), theCase.getName())); } /** * Gets the case that provides the model (the local drawables database and * the image gallery tables in the case database) for this controller. * * @return The case. */ public Case getCase() { return theCase; } /** * Gets the drawables database that is part of the model for this * controller. * * @return The drawables database. */ public DrawableDB getDrawablesDatabase() { return drawableDB; } /** * Gets the case database that provides part of the model for this * controller. * * @return The case database. */ public SleuthkitCase getCaseDatabase() { return caseDb; } public ReadOnlyBooleanProperty metaDataCollapsedProperty() { return metaDataCollapsed.getReadOnlyProperty(); } public void setMetaDataCollapsed(Boolean metaDataCollapsed) { this.metaDataCollapsed.set(metaDataCollapsed); } public DoubleProperty thumbnailSizeProperty() { return thumbnailSizeProp; } public ReadOnlyBooleanProperty regroupDisabledProperty() { return regroupDisabled.getReadOnlyProperty(); } public FileIDSelectionModel getSelectionModel() { return selectionModel; } public GroupManager getGroupManager() { return groupManager; } public void setListeningEnabled(boolean enabled) { synchronized (listeningEnabled) { listeningEnabled.set(enabled); } } public boolean isListeningEnabled() { synchronized (listeningEnabled) { return listeningEnabled.get(); } } /** * Sets a flag indicating whether the model is "stale" for any data source * in the current case. The model is a local drawables database and the * image gallery tables in the case database. * * @param isStale True if the model is "stale" for any data source in the * current case. */ @ThreadConfined(type = ThreadConfined.ThreadType.ANY) void setModelIsStale(Boolean isStale) { Platform.runLater(() -> { modelStale.set(isStale); }); } /** * Gets the boolean property that is set to true if the model is "stale" for * any data source in the current case. The model is a local drawables * database and the image gallery tables in the case database. * * @return The property that is set to true if the model is "stale" for any * data source in the current case. */ public ReadOnlyBooleanProperty modelIsStaleProperty() { return modelStale.getReadOnlyProperty(); } /** * Gets the state of the flag that is set if the Model is "stale" for any * data source in the case. The model is a local drawables database and the * image gallery tables in the case database. * * @return True if the model is "stale" for any data source in the current * case. */ @ThreadConfined(type = ThreadConfined.ThreadType.JFX) boolean modelIsStale() { return modelStale.get(); } /** * Gets the state of the image group display area in the UI. * * @return The current state. */ public GroupViewState getViewState() { return historyManager.getCurrentState(); } /** * Gets the state of the image group display area in the UI. * * @return The current state. */ public ReadOnlyObjectProperty viewStateProperty() { return historyManager.currentState(); } /** * Should the "forward" button on the history be enabled? * * @return True or false. */ public ReadOnlyBooleanProperty getCanAdvance() { return historyManager.getCanAdvance(); } /** * Should the "Back" button on the history be enabled? * * @return True or false. */ public ReadOnlyBooleanProperty getCanRetreat() { return historyManager.getCanRetreat(); } /** * Displays the passed in image group. Causes this group to get recorded in * the history queue and observers of the current state will be notified and * update their panels/widgets appropriately. * * @param newState */ @ThreadConfined(type = ThreadConfined.ThreadType.ANY) public void advance(GroupViewState newState) { historyManager.advance(newState); } /** * Display the next group in the "forward" history stack * * @return */ public GroupViewState advance() { return historyManager.advance(); } /** * Display the previous group in the "back" history stack * * @return */ public GroupViewState retreat() { return historyManager.retreat(); } @ThreadConfined(type = ThreadConfined.ThreadType.JFX) private void updateRegroupDisabled() { regroupDisabled.set((dbTaskQueueSize.get() > 0) || IngestManager.getInstance().isIngestRunning()); } /** * Rebuilds the DrawableDB database. * */ public void rebuildDrawablesDb() { // queue a rebuild task for each stale data source getStaleDataSourceIds().forEach(dataSourceObjId -> queueDBTask(new AddDrawableFilesTask(dataSourceObjId, this))); } /** * 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 or IN_PROGRESS 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 == getDrawablesDatabase()) || (null == getCaseDatabase())) { return staleDataSourceIds; } try { Map knownDataSourceIds = getDrawablesDatabase().getDataSourceDbBuildStatus(); List dataSources = getCaseDatabase().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(); switch (status) { case COMPLETE: case IN_PROGRESS: // not stale break; case REBUILT_STALE: staleDataSourceIds.add(t.getKey()); break; case UNKNOWN: try { // stale if there are files in CaseDB with MIME types if (hasFilesWithMimeType(t.getKey())) { staleDataSourceIds.add(t.getKey()); } } catch (TskCoreException ex) { logger.log(Level.SEVERE, "Error getting MIME types", ex); } break; } }); // 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; } } /** * Returns a map of all data source object ids, along with their DB build * status. * * This includes any data sources already in the table, and any data sources * that might have been added to the case, but are not in the datasources * table. * * @return map of data source object ids and their Db build status. */ public Map getAllDataSourcesDrawableDBStatus() { Map dataSourceStatusMap = new HashMap<>(); // no current case open to check if ((null == getDrawablesDatabase()) || (null == getCaseDatabase())) { return dataSourceStatusMap; } try { Map knownDataSourceIds = getDrawablesDatabase().getDataSourceDbBuildStatus(); List dataSources = getCaseDatabase().getDataSources(); Set caseDataSourceIds = new HashSet<>(); dataSources.stream().map(DataSource::getId).forEach(caseDataSourceIds::add); // collect all data sources already in the table knownDataSourceIds.entrySet().stream().forEach((Map.Entry t) -> { dataSourceStatusMap.put(t.getKey(), t.getValue()); }); // collect any new data sources in the case. caseDataSourceIds.forEach((Long id) -> { if (!knownDataSourceIds.containsKey(id)) { dataSourceStatusMap.put(id, DrawableDbBuildStatusEnum.UNKNOWN); } }); return dataSourceStatusMap; } catch (TskCoreException ex) { logger.log(Level.SEVERE, "Image Gallery failed to get data source DB status.", ex); return dataSourceStatusMap; } } public boolean hasTooManyFiles(DataSource datasource) throws TskCoreException { String whereClause = (datasource == null) ? "1 = 1" : "data_source_obj_id = " + datasource.getId(); return caseDb.countFilesWhere(whereClause) > FILE_LIMIT; } /** * Checks if the given data source has any files with no mimetype * * @param datasource * * @return true if the datasource has any files with no mime type * * @throws TskCoreException */ public boolean hasFilesWithNoMimeType(long dataSourceId) throws TskCoreException { // There are some special files/attributes in the root folder, like $BadClus:$Bad and $Security:$SDS // The IngestTasksScheduler does not push them down to the ingest modules, // and hence they do not have any assigned mimetype String whereClause = "data_source_obj_id = " + dataSourceId + " AND ( meta_type = " + TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_REG.getValue() + ")" + " AND ( mime_type IS NULL )" + " AND ( meta_addr >= 32 ) " + " AND ( parent_path <> '/' )" + " AND ( name NOT like '$%:%' )"; return caseDb.countFilesWhere(whereClause) > 0; } public boolean hasFilesWithMimeType(long dataSourceId) throws TskCoreException { String whereClause = "data_source_obj_id = " + dataSourceId + " AND ( meta_type = " + TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_REG.getValue() + ")" + " AND ( mime_type IS NOT NULL )"; return caseDb.countFilesWhere(whereClause) > 0; } synchronized private void shutDownDBExecutor() { dbExecutor.shutdownNow(); 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); } } private static ListeningExecutorService getNewDBExecutor() { return MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor( new ThreadFactoryBuilder().setNameFormat("DB-Worker-Thread-%d").build())); } /** * add InnerTask to the queue that the worker thread gets its work from * * @param bgTask */ public synchronized void queueDBTask(DrawableDbTask bgTask) { if (!dbExecutor.isShutdown()) { incrementQueueSize(); dbExecutor.submit(bgTask).addListener(this::decrementQueueSize, MoreExecutors.directExecutor()); } } private void incrementQueueSize() { Platform.runLater(() -> dbTaskQueueSize.set(dbTaskQueueSize.get() + 1)); } private void decrementQueueSize() { Platform.runLater(() -> dbTaskQueueSize.set(dbTaskQueueSize.get() - 1)); } public DrawableFile getFileFromID(Long fileID) throws TskCoreException { return drawableDB.getFileFromID(fileID); } public HashSetManager getHashSetManager() { return hashSetManager; } public CategoryManager getCategoryManager() { return categoryManager; } public DrawableTagsManager getTagsManager() { return tagsManager; } public UndoRedoManager getUndoManager() { return undoManager; } public ReadOnlyIntegerProperty getDBTasksQueueSizeProperty() { return dbTaskQueueSize.getReadOnlyProperty(); } public ThumbnailCache getThumbsCache() { return thumbnailCache; } /** * 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 file The file. * * @return True if the file is "drawable" and not "known", false otherwise. * * @throws FileTypeDetectorInitException If there is an error determining * the type of the file. */ private static boolean isDrawableAndNotKnown(AbstractFile abstractFile) throws FileTypeDetector.FileTypeDetectorInitException { return (abstractFile.getKnown() != TskData.FileKnown.KNOWN) && FileTypeUtils.isDrawable(abstractFile); } /** * A listener for ingest module application events. */ private class IngestModuleEventListener implements PropertyChangeListener { @Override public void propertyChange(PropertyChangeEvent event) { /* * Disable regrouping when ingest is running. */ Platform.runLater(ImageGalleryController.this::updateRegroupDisabled); if (isListeningEnabled() == false) { return; } /* * Updates when individual files are fully analyzed and artifacts * are added to a case are only done in "real time" on the host that * is running the ingest job. On a remote host, the updates are * deferred until the ingest job is complete. */ if (((AutopsyEvent) event).getSourceType() != AutopsyEvent.SourceType.LOCAL) { return; } String eventType = event.getPropertyName(); switch (IngestManager.IngestModuleEvent.valueOf(eventType)) { case FILE_DONE: AbstractFile file = (AbstractFile) event.getNewValue(); if (!file.isFile()) { return; } try { if (isDrawableAndNotKnown(file)) { queueDBTask(new UpdateDrawableFileTask(file, drawableDB)); } } catch (FileTypeDetector.FileTypeDetectorInitException ex) { 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 } break; case DATA_ADDED: ModuleDataEvent artifactAddedEvent = (ModuleDataEvent) event.getOldValue(); if (CollectionUtils.isNotEmpty(artifactAddedEvent.getArtifacts())) { for (BlackboardArtifact art : artifactAddedEvent.getArtifacts()) { if (artifactAddedEvent.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_METADATA_EXIF.getTypeID()) { drawableDB.addExifCache(art.getObjectID()); } else if (artifactAddedEvent.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID()) { drawableDB.addHashSetCache(art.getObjectID()); } } } break; default: break; } } } /** * A listener for case application events. */ private class CaseEventListener implements PropertyChangeListener { @Override public void propertyChange(PropertyChangeEvent event) { Case.Events eventType = Case.Events.valueOf(event.getPropertyName()); if (eventType == Case.Events.CURRENT_CASE) { if (event.getOldValue() != null) { // Case closed event if (topComponent != null) { topComponent.closeForCurrentCase(); } SwingUtilities.invokeLater(ImageGalleryTopComponent::closeTopComponent); } } else { switch (eventType) { case DATA_SOURCE_ADDED: if (((AutopsyEvent) event).getSourceType() == AutopsyEvent.SourceType.LOCAL) { Content newDataSource = (Content) event.getNewValue(); if (isListeningEnabled()) { drawableDB.insertOrUpdateDataSource(newDataSource.getId(), DrawableDB.DrawableDbBuildStatusEnum.UNKNOWN); } } break; case CONTENT_TAG_ADDED: final ContentTagAddedEvent tagAddedEvent = (ContentTagAddedEvent) event; long objId = tagAddedEvent.getAddedTag().getContent().getId(); drawableDB.addTagCache(objId); // TODO (JIRA-5216): Why add the tag to the cache before doing the in DB check? if (drawableDB.isInDB(objId)) { tagsManager.fireTagAddedEvent(tagAddedEvent); } break; case CONTENT_TAG_DELETED: final ContentTagDeletedEvent tagDeletedEvent = (ContentTagDeletedEvent) event; if (drawableDB.isInDB(tagDeletedEvent.getDeletedTagInfo().getContentID())) { tagsManager.fireTagDeletedEvent(tagDeletedEvent); } // TODO (JIRA-5216): Why not remove the tag from the cache? break; default: logger.log(Level.WARNING, String.format("Received %s event with no subscription", event.getPropertyName())); //NON-NLS break; } } } } /** * A listener for ingest job application events. */ 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 event) { /* * Disable regrouping when ingest is running. */ Platform.runLater(ImageGalleryController.this::updateRegroupDisabled); /* * Only handling data source analysis events. */ if (!(event instanceof DataSourceAnalysisEvent)) { return; } DataSourceAnalysisEvent dataSourceEvent = (DataSourceAnalysisEvent) event; Content dataSource = dataSourceEvent.getDataSource(); if (dataSource == null) { logger.log(Level.WARNING, String.format("Failed to handle %s event", event.getPropertyName())); //NON-NLS return; } long dataSourceObjId = dataSource.getId(); String eventType = dataSourceEvent.getPropertyName(); try { switch (IngestManager.IngestJobEvent.valueOf(eventType)) { case DATA_SOURCE_ANALYSIS_STARTED: handleDataSourceAnalysisStarted(dataSourceEvent); break; case DATA_SOURCE_ANALYSIS_COMPLETED: handleDataSourceAnalysisCompleted(dataSourceEvent); break; default: break; } } catch (TskCoreException ex) { logger.log(Level.SEVERE, String.format("Failed to handle %s event for %s (objId=%d)", dataSourceEvent.getPropertyName(), dataSource.getName(), dataSourceObjId), ex); } } } /** * Handles a data source analysis started event by adding the data source to * the drawables database. * * @param event The event. * * @throws TskCoreException If there is an error adding the data source to * the database. */ private void handleDataSourceAnalysisStarted(DataSourceAnalysisEvent event) throws TskCoreException { if (event.getSourceType() == AutopsyEvent.SourceType.LOCAL && isListeningEnabled()) { Content dataSource = event.getDataSource(); long dataSourceObjId = dataSource.getId(); if (drawableDB.getDataSourceDbBuildStatus(dataSourceObjId) != DrawableDB.DrawableDbBuildStatusEnum.COMPLETE) { drawableDB.insertOrUpdateDataSource(dataSource.getId(), DrawableDB.DrawableDbBuildStatusEnum.IN_PROGRESS); } drawableDB.buildFileMetaDataCache(); } } /** * Handles a data source analysis completed event by updating the state of * the data source stored in the drawables database if the event is local or * prompting the user to do a refresh if the event is remote. * * @param event The event. * * @throws TskCoreException If there is an error updating the state ot the * data source in the database. */ private void handleDataSourceAnalysisCompleted(DataSourceAnalysisEvent event) throws TskCoreException { if (event.getSourceType() == AutopsyEvent.SourceType.LOCAL) { Content dataSource = event.getDataSource(); long dataSourceObjId = dataSource.getId(); /* * This node just completed analysis of a data source. Set the state * of the local drawables database. */ if (isListeningEnabled()) { groupManager.resetCurrentPathGroup(); if (drawableDB.getDataSourceDbBuildStatus(dataSourceObjId) == DrawableDB.DrawableDbBuildStatusEnum.IN_PROGRESS) { // If at least one file in CaseDB has mime type, then set to COMPLETE // Otherwise, back to UNKNOWN since we assume file type module was not run DrawableDB.DrawableDbBuildStatusEnum datasourceDrawableDBStatus = hasFilesWithMimeType(dataSourceObjId) ? DrawableDB.DrawableDbBuildStatusEnum.COMPLETE : DrawableDB.DrawableDbBuildStatusEnum.UNKNOWN; drawableDB.insertOrUpdateDataSource(dataSource.getId(), datasourceDrawableDBStatus); } drawableDB.freeFileMetaDataCache(); } } else if (((AutopsyEvent) event).getSourceType() == AutopsyEvent.SourceType.REMOTE) { /* * 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. */ setModelIsStale(true); if (isListeningEnabled()) { SwingUtilities.invokeLater(() -> { if (ImageGalleryTopComponent.isImageGalleryOpen()) { 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: rebuildDrawablesDb(); break; case JOptionPane.NO_OPTION: case JOptionPane.CANCEL_OPTION: default: break; } } }); } } } }