/*
* Autopsy Forensic Browser
*
* Copyright 2013-16 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.imagegallery;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.logging.Level;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.concurrent.Worker;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javax.annotation.Nullable;
import javax.swing.SwingUtilities;
import org.netbeans.api.progress.ProgressHandle;
import org.openide.util.Cancellable;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent;
import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent;
import org.sleuthkit.autopsy.core.RuntimeProperties;
import org.sleuthkit.autopsy.coreutils.History;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.imagegallery.actions.UndoRedoManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableDB;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableTagsManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.HashSetManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState;
import org.sleuthkit.autopsy.imagegallery.gui.NoGroupsDialog;
import org.sleuthkit.autopsy.imagegallery.gui.Toolbar;
import org.sleuthkit.autopsy.ingest.IngestManager;
import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.datamodel.TskData;
/**
* Connects different parts of ImageGallery together and is hub for flow of
* control.
*/
public final class ImageGalleryController implements Executor {
private final Executor execDelegate = Executors.newSingleThreadExecutor();
private Runnable showTree;
private Toolbar toolbar;
@Override
public void execute(Runnable command) {
execDelegate.execute(command);
}
private static final Logger LOGGER = Logger.getLogger(ImageGalleryController.class.getName());
private final Region infoOverLayBackground = new Region() {
{
setBackground(new Background(new BackgroundFill(Color.GREY, CornerRadii.EMPTY, Insets.EMPTY)));
setOpacity(.4);
}
};
private static ImageGalleryController instance;
public static synchronized ImageGalleryController getDefault() {
if (instance == null) {
instance = new ImageGalleryController();
}
return instance;
}
private final History<GroupViewState> historyManager = new History<>();
private final UndoRedoManager undoManager = new UndoRedoManager();
/**
* true if Image Gallery should listen to ingest events, false if it should
* not listen to speed up ingest
*/
private final SimpleBooleanProperty listeningEnabled = new SimpleBooleanProperty(false);
private final ReadOnlyBooleanWrapper regroupDisabled = new ReadOnlyBooleanWrapper(false);
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private final ReadOnlyBooleanWrapper stale = new ReadOnlyBooleanWrapper(false);
private final ReadOnlyBooleanWrapper metaDataCollapsed = new ReadOnlyBooleanWrapper(false);
private final ReadOnlyDoubleWrapper thumbnailSize = new ReadOnlyDoubleWrapper(100);
private final FileIDSelectionModel selectionModel = new FileIDSelectionModel(this);
private DBWorkerThread dbWorkerThread;
private DrawableDB db;
private final GroupManager groupManager = new GroupManager(this);
private final HashSetManager hashSetManager = new HashSetManager();
private final CategoryManager categoryManager = new CategoryManager(this);
private final DrawableTagsManager tagsManager = new DrawableTagsManager(null);
private StackPane fullUIStackPane;
private StackPane centralStackPane;
private Node infoOverlay;
private SleuthkitCase sleuthKitCase;
public ReadOnlyBooleanProperty getMetaDataCollapsed() {
return metaDataCollapsed.getReadOnlyProperty();
}
public void setMetaDataCollapsed(Boolean metaDataCollapsed) {
this.metaDataCollapsed.set(metaDataCollapsed);
}
public ReadOnlyDoubleProperty thumbnailSizeProperty() {
return thumbnailSize.getReadOnlyProperty();
}
private GroupViewState getViewState() {
return historyManager.getCurrentState();
}
public ReadOnlyBooleanProperty regroupDisabled() {
return regroupDisabled.getReadOnlyProperty();
}
public ReadOnlyObjectProperty<GroupViewState> viewState() {
return historyManager.currentState();
}
public FileIDSelectionModel getSelectionModel() {
return selectionModel;
}
public GroupManager getGroupManager() {
return groupManager;
}
public DrawableDB getDatabase() {
return db;
}
public void setListeningEnabled(boolean enabled) {
synchronized (listeningEnabled) {
listeningEnabled.set(enabled);
}
}
boolean isListeningEnabled() {
synchronized (listeningEnabled) {
return listeningEnabled.get();
}
}
@ThreadConfined(type = ThreadConfined.ThreadType.ANY)
void setStale(Boolean b) {
Platform.runLater(() -> {
stale.set(b);
});
if (Case.isCaseOpen()) {
new PerCaseProperties(Case.getCurrentCase()).setConfigSetting(ImageGalleryModule.getModuleName(), PerCaseProperties.STALE, b.toString());
}
}
public ReadOnlyBooleanProperty stale() {
return stale.getReadOnlyProperty();
}
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
boolean isStale() {
return stale.get();
}
private ImageGalleryController() {
listeningEnabled.addListener((observable, oldValue, newValue) -> {
//if we just turned on listening and a case is open and that case is not up to date
if (newValue && !oldValue && Case.isCaseOpen() && ImageGalleryModule.isDrawableDBStale(Case.getCurrentCase())) {
//populate the db
queueDBWorkerTask(new CopyAnalyzedFiles(instance, db, sleuthKitCase));
}
});
groupManager.getAnalyzedGroups().addListener((Observable o) -> {
//analyzed groups is confined to JFX thread
if (Case.isCaseOpen()) {
checkForGroups();
}
});
groupManager.getUnSeenGroups().addListener((Observable observable) -> {
//if there are unseen groups and none being viewed
if (groupManager.getUnSeenGroups().isEmpty() == false && (getViewState() == null || getViewState().getGroup() == null)) {
advance(GroupViewState.tile(groupManager.getUnSeenGroups().get(0)), true);
}
});
viewState().addListener((Observable observable) -> {
//when the viewed group changes, clear the selection and the undo/redo history
selectionModel.clearSelection();
undoManager.clear();
});
regroupDisabled.addListener((Observable observable) -> {
checkForGroups();
});
IngestManager ingestManager = IngestManager.getInstance();
PropertyChangeListener ingestEventHandler =
propertyChangeEvent -> Platform.runLater(this::updateRegroupDisabled);
ingestManager.addIngestModuleEventListener(ingestEventHandler);
ingestManager.addIngestJobEventListener(ingestEventHandler);
queueSizeProperty.addListener(obs -> this.updateRegroupDisabled());
}
public ReadOnlyBooleanProperty getCanAdvance() {
return historyManager.getCanAdvance();
}
public ReadOnlyBooleanProperty getCanRetreat() {
return historyManager.getCanRetreat();
}
@ThreadConfined(type = ThreadConfined.ThreadType.ANY)
public void advance(GroupViewState newState, boolean forceShowTree) {
if (forceShowTree && showTree != null) {
showTree.run();
}
historyManager.advance(newState);
}
public GroupViewState advance() {
return historyManager.advance();
}
public GroupViewState retreat() {
return historyManager.retreat();
}
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private void updateRegroupDisabled() {
regroupDisabled.set((queueSizeProperty.get() > 0) || IngestManager.getInstance().isIngestRunning());
}
/**
* Check if there are any fully analyzed groups available from the
* GroupManager and remove blocking progress spinners if there are. If there
* aren't, add a blocking progress spinner with appropriate message.
*/
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
@NbBundle.Messages({"ImageGalleryController.noGroupsDlg.msg1=No groups are fully analyzed; but listening to ingest is disabled. "
+ " No groups will be available until ingest is finished and listening is re-enabled.",
"ImageGalleryController.noGroupsDlg.msg2=No groups are fully analyzed yet, but ingest is still ongoing. Please Wait.",
"ImageGalleryController.noGroupsDlg.msg3=No groups are fully analyzed yet, but image / video data is still being populated. Please Wait.",
"ImageGalleryController.noGroupsDlg.msg4=There are no images/videos available from the added datasources; but listening to ingest is disabled. "
+ " No groups will be available until ingest is finished and listening is re-enabled.",
"ImageGalleryController.noGroupsDlg.msg5=There are no images/videos in the added datasources.",
"ImageGalleryController.noGroupsDlg.msg6=There are no fully analyzed groups to display:"
+ " the current Group By setting resulted in no groups, "
+ "or no groups are fully analyzed but ingest is not running."})
public void checkForGroups() {
if (groupManager.getAnalyzedGroups().isEmpty()) {
if (IngestManager.getInstance().isIngestRunning()) {
if (listeningEnabled.get() == false) {
replaceNotification(fullUIStackPane,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg1()));
} else {
replaceNotification(fullUIStackPane,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg2(),
new ProgressIndicator()));
}
} else if (queueSizeProperty.get() > 0) {
replaceNotification(fullUIStackPane,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg3(),
new ProgressIndicator()));
} else if (db != null && db.countAllFiles() <= 0) { // there are no files in db
if (listeningEnabled.get() == false) {
replaceNotification(fullUIStackPane,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg4()));
} else {
replaceNotification(fullUIStackPane,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg5()));
}
} else if (!groupManager.isRegrouping()) {
replaceNotification(centralStackPane,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg6()));
}
} else {
clearNotification();
}
}
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private void clearNotification() {
//remove the ingest spinner
if (fullUIStackPane != null) {
fullUIStackPane.getChildren().remove(infoOverlay);
}
//remove the ingest spinner
if (centralStackPane != null) {
centralStackPane.getChildren().remove(infoOverlay);
}
}
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private void replaceNotification(StackPane stackPane, Node newNode) {
clearNotification();
infoOverlay = new StackPane(infoOverLayBackground, newNode);
if (stackPane != null) {
stackPane.getChildren().add(infoOverlay);
}
}
synchronized private DBWorkerThread restartWorker() {
if (dbWorkerThread == null) {
dbWorkerThread = new DBWorkerThread(this);
dbWorkerThread.start();
} else {
// Keep using the same worker thread if one exists
}
return dbWorkerThread;
}
/**
* configure the controller for a specific case.
*
* @param theNewCase the case to configure the controller for
*/
public synchronized void setCase(Case theNewCase) {
if (Objects.nonNull(theNewCase)) {
this.sleuthKitCase = theNewCase.getSleuthkitCase();
this.db = DrawableDB.getDrawableDB(ImageGalleryModule.getModuleOutputDir(theNewCase), this);
setListeningEnabled(ImageGalleryModule.isEnabledforCase(theNewCase));
setStale(ImageGalleryModule.isDrawableDBStale(theNewCase));
// if we add this line icons are made as files are analyzed rather than on demand.
// db.addUpdatedFileListener(IconCache.getDefault());
restartWorker();
historyManager.clear();
groupManager.setDB(db);
hashSetManager.setDb(db);
categoryManager.setDb(db);
tagsManager.setAutopsyTagsManager(theNewCase.getServices().getTagsManager());
tagsManager.registerListener(groupManager);
tagsManager.registerListener(categoryManager);
} else {
reset();
}
}
/**
* reset the state of the controller (eg if the case is closed)
*/
public synchronized void reset() {
LOGGER.info("resetting ImageGalleryControler to initial state."); //NON-NLS
selectionModel.clearSelection();
setListeningEnabled(false);
ThumbnailCache.getDefault().clearCache();
historyManager.clear();
groupManager.clear();
tagsManager.clearFollowUpTagName();
tagsManager.unregisterListener(groupManager);
tagsManager.unregisterListener(categoryManager);
dbWorkerThread.cancel();
dbWorkerThread = null;
dbWorkerThread = restartWorker();
if (toolbar != null) {
toolbar.reset();
}
if (db != null) {
db.closeDBCon();
}
db = null;
}
/**
* add InnerTask to the queue that the worker thread gets its work from
*
* @param innerTask
*/
public synchronized void queueDBWorkerTask(BackgroundTask innerTask) {
if (dbWorkerThread == null) {
dbWorkerThread = restartWorker();
}
dbWorkerThread.addTask(innerTask);
}
@Nullable
synchronized public DrawableFile getFileFromId(Long fileID) throws TskCoreException {
if (Objects.isNull(db)) {
LOGGER.log(Level.WARNING, "Could not get file from id, no DB set. The case is probably closed."); //NON-NLS
return null;
}
return db.getFileFromID(fileID);
}
public void setStacks(StackPane fullUIStack, StackPane centralStack) {
fullUIStackPane = fullUIStack;
this.centralStackPane = centralStack;
Platform.runLater(this::checkForGroups);
}
public synchronized void setToolbar(Toolbar toolbar) {
if (this.toolbar != null) {
throw new IllegalStateException("Can not set the toolbar a second time!");
}
this.toolbar = toolbar;
thumbnailSize.bind(toolbar.thumbnailSizeProperty());
}
public ReadOnlyDoubleProperty regroupProgress() {
return groupManager.regroupProgress();
}
/**
* invoked by {@link OnStart} to make sure that the ImageGallery listeners
* get setup as early as possible, and do other setup stuff.
*/
void onStart() {
Platform.setImplicitExit(false);
LOGGER.info("setting up ImageGallery listeners"); //NON-NLS
//TODO can we do anything usefull in an InjestJobEventListener?
//IngestManager.getInstance().addIngestJobEventListener((PropertyChangeEvent evt) -> {});
IngestManager.getInstance().addIngestModuleEventListener(new IngestModuleEventListener());
Case.addPropertyChangeListener(new CaseEventListener());
}
public HashSetManager getHashSetManager() {
return hashSetManager;
}
public CategoryManager getCategoryManager() {
return categoryManager;
}
public DrawableTagsManager getTagsManager() {
return tagsManager;
}
public void setShowTree(Runnable showTree) {
this.showTree = showTree;
}
public UndoRedoManager getUndoManager() {
return undoManager;
}
public ReadOnlyIntegerProperty getDBTasksQueueSizeProperty() {
return queueSizeProperty.getReadOnlyProperty();
}
private final ReadOnlyIntegerWrapper queueSizeProperty = new ReadOnlyIntegerWrapper(0);
// @@@ review this class for synchronization issues (i.e. reset and cancel being called, add, etc.)
static private class DBWorkerThread extends Thread implements Cancellable {
private final ImageGalleryController controller;
DBWorkerThread(ImageGalleryController controller) {
super("DB-Worker-Thread");
setDaemon(false);
this.controller = controller;
}
// true if the process was requested to stop. Currently no way to reset it
private volatile boolean cancelled = false;
// list of tasks to run
private final BlockingQueue<BackgroundTask> workQueue = new LinkedBlockingQueue<>();
/**
* Cancel all of the queued up tasks and the currently scheduled task.
* Note that after you cancel, you cannot submit new jobs to this
* thread.
*/
@Override
public boolean cancel() {
cancelled = true;
for (BackgroundTask it : workQueue) {
it.cancel();
}
workQueue.clear();
int size = workQueue.size();
Platform.runLater(() -> controller.queueSizeProperty.set(size));
return true;
}
/**
* Add a task for the worker thread to perform
*
* @param it
*/
public void addTask(BackgroundTask it) {
workQueue.add(it);
int size = workQueue.size();
Platform.runLater(() -> controller.queueSizeProperty.set(size));
}
@Override
public void run() {
// nearly infinite loop waiting for tasks
while (true) {
if (cancelled || isInterrupted()) {
return;
}
try {
BackgroundTask it = workQueue.take();
if (it.isCancelled() == false) {
it.run();
}
int size = workQueue.size();
Platform.runLater(() -> controller.queueSizeProperty.set(size));
} catch (InterruptedException ex) {
LOGGER.log(Level.SEVERE, "Failed to run DB worker thread", ex); //NON-NLS
}
}
}
}
public synchronized SleuthkitCase getSleuthKitCase() {
return sleuthKitCase;
}
/**
* Abstract base class for task to be done on {@link DBWorkerThread}
*/
@NbBundle.Messages({"ImageGalleryController.InnerTask.progress.name=progress",
"ImageGalleryController.InnerTask.message.name=status"})
static public abstract class BackgroundTask implements Runnable, Cancellable {
private final SimpleObjectProperty<Worker.State> state = new SimpleObjectProperty<>(Worker.State.READY);
private final SimpleDoubleProperty progress = new SimpleDoubleProperty(this, Bundle.ImageGalleryController_InnerTask_progress_name());
private final SimpleStringProperty message = new SimpleStringProperty(this, Bundle.ImageGalleryController_InnerTask_message_name());
protected BackgroundTask() {
}
public double getProgress() {
return progress.get();
}
public final void updateProgress(Double workDone) {
this.progress.set(workDone);
}
public String getMessage() {
return message.get();
}
public final void updateMessage(String Status) {
this.message.set(Status);
}
public SimpleDoubleProperty progressProperty() {
return progress;
}
public SimpleStringProperty messageProperty() {
return message;
}
public Worker.State getState() {
return state.get();
}
public ReadOnlyObjectProperty<Worker.State> stateProperty() {
return new ReadOnlyObjectWrapper<>(state.get());
}
@Override
public synchronized boolean cancel() {
updateState(Worker.State.CANCELLED);
return true;
}
protected void updateState(Worker.State newState) {
state.set(newState);
}
protected synchronized boolean isCancelled() {
return getState() == Worker.State.CANCELLED;
}
}
/**
* Abstract base class for tasks associated with a file in the database
*/
static public abstract class FileTask extends BackgroundTask {
private final AbstractFile file;
private final DrawableDB taskDB;
public DrawableDB getTaskDB() {
return taskDB;
}
public AbstractFile getFile() {
return file;
}
public FileTask(AbstractFile f, DrawableDB taskDB) {
super();
this.file = f;
this.taskDB = taskDB;
}
}
/**
* task that updates one file in database with results from ingest
*/
static private class UpdateFileTask extends FileTask {
UpdateFileTask(AbstractFile f, DrawableDB taskDB) {
super(f, taskDB);
}
/**
* Update a file in the database
*/
@Override
public void run() {
try {
DrawableFile drawableFile = DrawableFile.create(getFile(), true, false);
getTaskDB().updateFile(drawableFile);
} catch (NullPointerException ex) {
// This is one of the places where we get many errors if the case is closed during processing.
// We don't want to print out a ton of exceptions if this is the case.
if (Case.isCaseOpen()) {
Logger.getLogger(UpdateFileTask.class.getName()).log(Level.SEVERE, "Error in UpdateFile task"); //NON-NLS
}
}
}
}
/**
* task that updates one file in database with results from ingest
*/
static private class RemoveFileTask extends FileTask {
RemoveFileTask(AbstractFile f, DrawableDB taskDB) {
super(f, taskDB);
}
/**
* Update a file in the database
*/
@Override
public void run() {
try {
getTaskDB().removeFile(getFile().getId());
} catch (NullPointerException ex) {
// This is one of the places where we get many errors if the case is closed during processing.
// We don't want to print out a ton of exceptions if this is the case.
if (Case.isCaseOpen()) {
Logger.getLogger(RemoveFileTask.class.getName()).log(Level.SEVERE, "Case was closed out from underneath RemoveFile task"); //NON-NLS
}
}
}
}
@NbBundle.Messages({"BulkTask.committingDb.status=committing image/video database",
"BulkTask.stopCopy.status=Stopping copy to drawable db task.",
"BulkTask.errPopulating.errMsg=There was an error populating Image Gallery database."})
abstract static private class BulkTransferTask extends BackgroundTask {
static private final String FILE_EXTENSION_CLAUSE =
"(name LIKE '%." //NON-NLS
+ String.join("' OR name LIKE '%.", FileTypeUtils.getAllSupportedExtensions()) //NON-NLS
+ "')";
static private final String MIMETYPE_CLAUSE =
"(mime_type LIKE '" //NON-NLS
+ String.join("' OR mime_type LIKE '", FileTypeUtils.getAllSupportedMimeTypes()) //NON-NLS
+ "') ";
static final String DRAWABLE_QUERY =
//grab files with supported extension
"(" + FILE_EXTENSION_CLAUSE
//grab files with supported mime-types
+ " OR " + MIMETYPE_CLAUSE //NON-NLS
//grab files with image or video mime-types even if we don't officially support them
+ " OR mime_type LIKE 'video/%' OR mime_type LIKE 'image/%' )"; //NON-NLS
final ImageGalleryController controller;
final DrawableDB taskDB;
final SleuthkitCase tskCase;
ProgressHandle progressHandle;
BulkTransferTask(ImageGalleryController controller, DrawableDB taskDB, SleuthkitCase tskCase) {
this.controller = controller;
this.taskDB = taskDB;
this.tskCase = tskCase;
}
abstract void cleanup(boolean success);
abstract List<AbstractFile> getFiles() throws TskCoreException;
abstract void processFile(final AbstractFile f, DrawableDB.DrawableTransaction tr) throws TskCoreException;
@Override
public void run() {
progressHandle = getInitialProgressHandle();
progressHandle.start();
updateMessage(Bundle.CopyAnalyzedFiles_populatingDb_status());
try {
//grab all files with supported extension or detected mime types
final List<AbstractFile> files = getFiles();
progressHandle.switchToDeterminate(files.size());
updateProgress(0.0);
//do in transaction
DrawableDB.DrawableTransaction tr = taskDB.beginTransaction();
int workDone = 0;
for (final AbstractFile f : files) {
if (isCancelled()) {
LOGGER.log(Level.WARNING, "Task cancelled: not all contents may be transfered to drawable database."); //NON-NLS
progressHandle.finish();
break;
}
processFile(f, tr);
workDone++;
progressHandle.progress(f.getName(), workDone);
updateProgress(workDone - 1 / (double) files.size());
updateMessage(f.getName());
}
progressHandle.finish();
progressHandle = ProgressHandle.createHandle(Bundle.BulkTask_committingDb_status());
updateMessage(Bundle.BulkTask_committingDb_status());
updateProgress(1.0);
progressHandle.start();
taskDB.commitTransaction(tr, true);
} catch (TskCoreException ex) {
progressHandle.progress(Bundle.BulkTask_stopCopy_status());
LOGGER.log(Level.WARNING, "Stopping copy to drawable db task. Failed to transfer all database contents", ex); //NON-NLS
MessageNotifyUtil.Notify.warn(Bundle.BulkTask_errPopulating_errMsg(), ex.getMessage());
cleanup(false);
return;
} finally {
progressHandle.finish();
updateMessage("");
updateProgress(-1.0);
}
cleanup(true);
}
abstract ProgressHandle getInitialProgressHandle();
}
/**
* Task that runs when image gallery listening is (re) enabled.
*
* Grabs all files with supported image/video mime types or extensions, and
* adds them to the Drawable DB. Uses the presence of a mimetype as an
* approximation to 'analyzed'.
*/
@NbBundle.Messages({"CopyAnalyzedFiles.committingDb.status=committing image/video database",
"CopyAnalyzedFiles.stopCopy.status=Stopping copy to drawable db task.",
"CopyAnalyzedFiles.errPopulating.errMsg=There was an error populating Image Gallery database."})
static private class CopyAnalyzedFiles extends BulkTransferTask {
CopyAnalyzedFiles(ImageGalleryController controller, DrawableDB taskDB, SleuthkitCase tskCase) {
super(controller, taskDB, tskCase);
}
@Override
protected void cleanup(boolean success) {
controller.setStale(!success);
}
@Override
List<AbstractFile> getFiles() throws TskCoreException {
return tskCase.findAllFilesWhere(DRAWABLE_QUERY);
}
@Override
void processFile(AbstractFile f, DrawableDB.DrawableTransaction tr) throws TskCoreException {
final boolean known = f.getKnown() == TskData.FileKnown.KNOWN;
if (known) {
taskDB.removeFile(f.getId(), tr); //remove known files
} else {
try {
if (FileTypeUtils.hasDrawableMIMEType(f)) { //supported mimetype => analyzed
taskDB.updateFile(DrawableFile.create(f, true, false), tr);
} else { //unsupported mimtype => analyzed but shouldn't include
taskDB.removeFile(f.getId(), tr);
}
} catch (FileTypeDetector.FileTypeDetectorInitException ex) {
throw new RuntimeException(ex);
}
}
}
@Override
@NbBundle.Messages({"CopyAnalyzedFiles.populatingDb.status=populating analyzed image/video database",})
ProgressHandle getInitialProgressHandle() {
return ProgressHandle.createHandle(Bundle.CopyAnalyzedFiles_populatingDb_status(), this);
}
}
/**
* Copy files from a newly added data source into the DB. Get all "drawable"
* files, based on extension and mime-type. After ingest we use file type id
* module and if necessary jpeg/png signature matching to add/remove files
*
* TODO: create methods to simplify progress value/text updates to both
* netbeans and ImageGallery progress/status
*/
@NbBundle.Messages({"PrePopulateDataSourceFiles.committingDb.status=committing image/video database"})
static private class PrePopulateDataSourceFiles extends BulkTransferTask {
private static final Logger LOGGER = Logger.getLogger(PrePopulateDataSourceFiles.class.getName());
private final Content dataSource;
/**
*
* @param dataSourceId Data source object ID
*/
PrePopulateDataSourceFiles(Content dataSource, ImageGalleryController controller, DrawableDB taskDB, SleuthkitCase tskCase) {
super(controller, taskDB, tskCase);
this.dataSource = dataSource;
}
@Override
protected void cleanup(boolean success) {
}
@Override
void processFile(final AbstractFile f, DrawableDB.DrawableTransaction tr) {
taskDB.insertFile(DrawableFile.create(f, false, false), tr);
}
@Override
List<AbstractFile> getFiles() throws TskCoreException {
long datasourceID = dataSource.getDataSource().getId();
return tskCase.findAllFilesWhere("data_source_obj_id = " + datasourceID + " AND " + DRAWABLE_QUERY);
}
@Override
@NbBundle.Messages({"PrePopulateDataSourceFiles.prepopulatingDb.status=prepopulating image/video database",})
ProgressHandle getInitialProgressHandle() {
return ProgressHandle.createHandle(Bundle.PrePopulateDataSourceFiles_prepopulatingDb_status(), this);
}
}
private class IngestModuleEventListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (RuntimeProperties.coreComponentsAreActive() == false) {
/*
* Running in "headless" mode, no need to process any events.
* This cannot be done earlier because the switch to core
* components inactive may not have been made at start up.
*/
IngestManager.getInstance().removeIngestModuleEventListener(this);
return;
}
switch (IngestManager.IngestModuleEvent.valueOf(evt.getPropertyName())) {
case CONTENT_CHANGED:
//TODO: do we need to do anything here? -jm
case DATA_ADDED:
/*
* we could listen to DATA events and progressivly update
* files, and get data from DataSource ingest modules, but
* given that most modules don't post new artifacts in the
* events and we would have to query for them, without
* knowing which are the new ones, we just ignore these
* events for now. The relevant data should all be captured
* by file done event, anyways -jm
*/
break;
case FILE_DONE:
/**
* getOldValue has fileID getNewValue has
* {@link Abstractfile}
*/
AbstractFile file = (AbstractFile) evt.getNewValue();
if (isListeningEnabled()) {
if (file.isFile()) {
try {
if (ImageGalleryModule.isDrawableAndNotKnown(file)) {
//this file should be included and we don't already know about it from hash sets (NSRL)
queueDBWorkerTask(new UpdateFileTask(file, db));
} else if (FileTypeUtils.getAllSupportedExtensions().contains(file.getNameExtension())) {
//doing this check results in fewer tasks queued up, and faster completion of db update
//this file would have gotten scooped up in initial grab, but actually we don't need it
queueDBWorkerTask(new RemoveFileTask(file, db));
}
} catch (TskCoreException | FileTypeDetector.FileTypeDetectorInitException ex) {
//TODO: What to do here?
LOGGER.log(Level.SEVERE, "Unable to determine if file is drawable and not known. Not making any changes to DB", ex); //NON-NLS
MessageNotifyUtil.Notify.error("Image Gallery Error",
"Unable to determine if file is drawable and not known. Not making any changes to DB. See the logs for details.");
}
}
} else { //TODO: keep track of what we missed for later
setStale(true);
}
break;
}
}
}
private class CaseEventListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (RuntimeProperties.coreComponentsAreActive() == false) {
/*
* Running in "headless" mode, no need to process any events.
* This cannot be done earlier because the switch to core
* components inactive may not have been made at start up.
*/
Case.removePropertyChangeListener(this);
return;
}
switch (Case.Events.valueOf(evt.getPropertyName())) {
case CURRENT_CASE:
Case newCase = (Case) evt.getNewValue();
if (newCase != null) { // case has been opened
setCase(newCase); //connect db, groupmanager, start worker thread
} else { // case is closing
//close window, reset everything
SwingUtilities.invokeLater(ImageGalleryTopComponent::closeTopComponent);
reset();
}
break;
case DATA_SOURCE_ADDED:
//copy all file data to drawable databse
Content newDataSource = (Content) evt.getNewValue();
if (isListeningEnabled()) {
queueDBWorkerTask(new PrePopulateDataSourceFiles(newDataSource, ImageGalleryController.this, getDatabase(), getSleuthKitCase()));
} else {//TODO: keep track of what we missed for later
setStale(true);
}
break;
case CONTENT_TAG_ADDED:
final ContentTagAddedEvent tagAddedEvent = (ContentTagAddedEvent) evt;
if (getDatabase().isInDB(tagAddedEvent.getAddedTag().getContent().getId())) {
getTagsManager().fireTagAddedEvent(tagAddedEvent);
}
break;
case CONTENT_TAG_DELETED:
final ContentTagDeletedEvent tagDeletedEvent = (ContentTagDeletedEvent) evt;
if (getDatabase().isInDB(tagDeletedEvent.getDeletedTagInfo().getContentID())) {
getTagsManager().fireTagDeletedEvent(tagDeletedEvent);
}
break;
}
}
}
}