org.sleuthkit.autopsy.imageanalyzer.ImageAnalyzerController.java Source code

Java tutorial

Introduction

Here is the source code for org.sleuthkit.autopsy.imageanalyzer.ImageAnalyzerController.java

Source

/*
 * Autopsy Forensic Browser
 *
 * Copyright 2013 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.imageanalyzer;

import java.beans.PropertyChangeEvent;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.BlockingQueue;
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.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
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.concurrent.GuardedBy;
import javax.swing.SwingUtilities;
import org.apache.commons.lang3.StringUtils;
import org.netbeans.api.progress.ProgressHandle;
import org.netbeans.api.progress.ProgressHandleFactory;
import org.openide.util.Exceptions;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.coreutils.History;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableDB;
import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile;
import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupManager;
import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupViewState;
import org.sleuthkit.autopsy.imageanalyzer.gui.NoGroupsDialog;
import org.sleuthkit.autopsy.imageanalyzer.gui.SummaryTablePane;
import org.sleuthkit.autopsy.imageanalyzer.gui.Toolbar;
import org.sleuthkit.autopsy.ingest.IngestManager;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.BlackboardArtifact;
import org.sleuthkit.datamodel.BlackboardAttribute;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.datamodel.TskData;

/**
 * Connects different parts of ImageAnalyzer together and is hub for flow of
 * control.
 */
public final class ImageAnalyzerController {

    private static final Logger LOGGER = Logger.getLogger(ImageAnalyzerController.class.getName());

    private final Region infoOverLayBackground = new Region() {
        {
            setBackground(new Background(new BackgroundFill(Color.GREY, CornerRadii.EMPTY, Insets.EMPTY)));
            setOpacity(.4);
        }
    };

    private static ImageAnalyzerController instance;

    public static synchronized ImageAnalyzerController getDefault() {
        if (instance == null) {
            instance = new ImageAnalyzerController();
        }
        return instance;
    }

    @GuardedBy("this")
    private final History<GroupViewState> historyManager = new History<>();

    private final ReadOnlyBooleanWrapper listeningEnabled = new ReadOnlyBooleanWrapper(false);

    private final ReadOnlyIntegerWrapper queueSizeProperty = new ReadOnlyIntegerWrapper(0);

    private final ReadOnlyBooleanWrapper regroupDisabled = new ReadOnlyBooleanWrapper(false);

    private final ReadOnlyBooleanWrapper stale = new ReadOnlyBooleanWrapper(false);

    private final ReadOnlyBooleanWrapper metaDataCollapsed = new ReadOnlyBooleanWrapper(false);

    private final FileIDSelectionModel selectionModel = FileIDSelectionModel.getInstance();

    private DBWorkerThread dbWorkerThread;

    private DrawableDB db;

    private final GroupManager groupManager = new GroupManager(this);

    private StackPane fullUIStackPane;

    private StackPane centralStackPane;

    private Node infoOverlay;

    public ReadOnlyBooleanProperty getMetaDataCollapsed() {
        return metaDataCollapsed.getReadOnlyProperty();
    }

    public void setMetaDataCollapsed(Boolean metaDataCollapsed) {
        this.metaDataCollapsed.set(metaDataCollapsed);
    }

    private GroupViewState getViewState() {
        return historyManager.getCurrentState();
    }

    public ReadOnlyBooleanProperty regroupDisabled() {
        return regroupDisabled.getReadOnlyProperty();
    }

    public ReadOnlyObjectProperty<GroupViewState> viewState() {
        return historyManager.currentState();
    }

    public synchronized FileIDSelectionModel getSelectionModel() {

        return selectionModel;
    }

    public GroupManager getGroupManager() {
        return groupManager;
    }

    public void setListeningEnabled(boolean enabled) {
        listeningEnabled.set(enabled);
    }

    ReadOnlyBooleanProperty listeningEnabled() {
        return listeningEnabled.getReadOnlyProperty();
    }

    boolean isListeningEnabled() {
        return listeningEnabled.get();
    }

    void setStale(Boolean b) {
        Platform.runLater(() -> {
            stale.set(b);
        });
        if (Case.isCaseOpen()) {
            new PerCaseProperties(Case.getCurrentCase()).setConfigSetting(ImageAnalyzerModule.MODULE_NAME,
                    PerCaseProperties.STALE, b.toString());
        }
    }

    public ReadOnlyBooleanProperty stale() {
        return stale.getReadOnlyProperty();
    }

    boolean isStale() {
        return stale.get();
    }

    private ImageAnalyzerController() {

        listeningEnabled.addListener(
                (ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> {
                    if (newValue && !oldValue && Case.existsCurrentCase()
                            && ImageAnalyzerModule.isCaseStale(Case.getCurrentCase())) {
                        queueDBWorkerTask(new CopyAnalyzedFiles());
                    }
                });

        groupManager.getAnalyzedGroups().addListener((Observable o) -> {
            checkForGroups();
        });

        groupManager.getUnSeenGroups().addListener((Observable observable) -> {
            //if there are unseen groups and none being viewed
            if (groupManager.getUnSeenGroups().size() > 0
                    && (getViewState() == null || getViewState().getGroup() == null)) {
                advance(GroupViewState.tile(groupManager.getUnSeenGroups().get(0)));
            }
        });

        viewState().addListener((Observable observable) -> {
            selectionModel.clearSelection();
        });

        regroupDisabled.addListener((Observable observable) -> {
            checkForGroups();
        });

        IngestManager.getInstance().addIngestModuleEventListener((PropertyChangeEvent evt) -> {
            Platform.runLater(this::updateRegroupDisabled);
        });
        IngestManager.getInstance().addIngestJobEventListener((PropertyChangeEvent evt) -> {
            Platform.runLater(this::updateRegroupDisabled);
        });
        //        metaDataCollapsed.bind(Toolbar.getDefault().showMetaDataProperty());
    }

    synchronized public ReadOnlyBooleanProperty getCanAdvance() {
        return historyManager.getCanAdvance();
    }

    synchronized public ReadOnlyBooleanProperty getCanRetreat() {
        return historyManager.getCanRetreat();
    }

    synchronized public void advance(GroupViewState newState) {
        historyManager.advance(newState);
    }

    synchronized public GroupViewState advance() {
        return historyManager.advance();
    }

    synchronized public GroupViewState retreat() {
        return historyManager.retreat();
    }

    private void updateRegroupDisabled() {
        regroupDisabled
                .set(getFileUpdateQueueSizeProperty().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.
     */
    public final void checkForGroups() {
        if (groupManager.getAnalyzedGroups().isEmpty()) {
            if (IngestManager.getInstance().isIngestRunning()) {
                if (listeningEnabled.get() == false) {
                    replaceNotification(fullUIStackPane,
                            new NoGroupsDialog("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."));
                } else {
                    replaceNotification(fullUIStackPane,
                            new NoGroupsDialog(
                                    "No groups are fully analyzed yet, but ingest is still ongoing.  Please Wait.",
                                    new ProgressIndicator()));
                }

            } else if (getFileUpdateQueueSizeProperty().get() > 0) {
                replaceNotification(fullUIStackPane, new NoGroupsDialog(
                        "No groups are fully analyzed yet, but image / video data is still being populated.  Please Wait.",
                        new ProgressIndicator()));
            } else if (db != null && db.countAllFiles() <= 0) { // there are no files in db
                replaceNotification(fullUIStackPane,
                        new NoGroupsDialog("There are no images/videos in the added datasources."));

            } else if (!groupManager.isRegrouping()) {
                replaceNotification(centralStackPane,
                        new NoGroupsDialog("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."));
            }

        } else {
            clearNotification();
        }
    }

    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);
        }
    }

    private void replaceNotification(StackPane stackPane, Node newNode) {
        clearNotification();

        infoOverlay = new StackPane(infoOverLayBackground, newNode);
        if (stackPane != null) {
            stackPane.getChildren().add(infoOverlay);
        }
    }

    private void restartWorker() {
        if (dbWorkerThread != null) {
            dbWorkerThread.cancelAllTasks();
        }
        dbWorkerThread = new DBWorkerThread();

        getFileUpdateQueueSizeProperty().addListener((Observable o) -> {
            Platform.runLater(this::updateRegroupDisabled);
        });

        Thread th = new Thread(dbWorkerThread);
        th.setDaemon(false); // we want it to go away when it is done
        th.start();
    }

    /**
     * onStart the controller for a specific case.
     *
     * @param c
     */
    public synchronized void setCase(Case c) {

        this.db = DrawableDB.getDrawableDB(c.getCaseDirectory(), this);

        setListeningEnabled(ImageAnalyzerModule.isEnabledforCase(c));
        setStale(ImageAnalyzerModule.isCaseStale(c));

        // 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);
        SummaryTablePane.getDefault().handleCategoryChanged(Collections.emptyList());
    }

    /**
     * reset the state of the controller (eg if the case is closed)
     */
    public synchronized void reset() {
        LOGGER.info("resetting ImageAnalyzerControler to initial state.");
        selectionModel.clearSelection();
        Platform.runLater(() -> {
            historyManager.clear();
        });

        Toolbar.getDefault().reset();
        groupManager.clear();
        if (db != null) {
            db.closeDBCon();
        }
        db = null;
    }

    /**
     * add InnerTask to the queue that the worker thread gets its work from
     *
     * @param innerTask
     */
    final void queueDBWorkerTask(InnerTask innerTask) {
        // @@@ We could make a lock for the worker thread
        if (dbWorkerThread == null) {
            restartWorker();
        }
        dbWorkerThread.addTask(innerTask);
    }

    public DrawableFile<?> getFileFromId(Long fileID) throws TskCoreException {
        return db.getFileFromID(fileID);
    }

    public void setStacks(StackPane fullUIStack, StackPane centralStack) {
        fullUIStackPane = fullUIStack;
        this.centralStackPane = centralStack;
        Platform.runLater(this::checkForGroups);
    }

    public final ReadOnlyIntegerProperty getFileUpdateQueueSizeProperty() {
        return queueSizeProperty.getReadOnlyProperty();
    }

    public ReadOnlyDoubleProperty regroupProgress() {
        return groupManager.regroupProgress();
    }

    /** invoked by {@link OnStart} to make sure that the ImageAnalyzer listeners
     * get setup as early as possible, and do other setup stuff. */
    void onStart() {
        Platform.setImplicitExit(false);
        LOGGER.info("setting up ImageAnalyzer listeners");
        //TODO can we do anything usefull in an InjestJobEventListener?
        //IngestManager.getInstance().addIngestJobEventListener((PropertyChangeEvent evt) -> {});
        IngestManager.getInstance().addIngestModuleEventListener((PropertyChangeEvent evt) -> {
            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 (ImageAnalyzerModule.isSupportedAndNotKnown(file)) {
                        //this file should be included and we don't already know about it from hash sets (NSRL)
                        queueDBWorkerTask(new UpdateFileTask(file));
                    } else if (ImageAnalyzerModule.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));
                    }
                } else { //TODO: keep track of what we missed for later
                    setStale(true);
                }
                break;
            }
        });
        Case.addPropertyChangeListener((PropertyChangeEvent evt) -> {
            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(ImageAnalyzerTopComponent::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.getId()));
                } else {//TODO: keep track of what we missed for later
                    setStale(true);
                }
                break;
            }
        });
    }

    // @@@ REVIEW IF THIS SHOLD BE STATIC...
    //TODO: concept seems like  the controller deal with how much work to do at a given time
    // @@@ review this class for synchronization issues (i.e. reset and cancel being called, add, etc.)
    private class DBWorkerThread implements Runnable {

        // 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<InnerTask> 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.
         */
        public void cancelAllTasks() {
            cancelled = true;
            for (InnerTask it : workQueue) {
                it.cancel();
            }
            workQueue.clear();
            queueSizeProperty.set(workQueue.size());
        }

        /**
         * Add a task for the worker thread to perform
         *
         * @param it
         */
        public void addTask(InnerTask it) {
            workQueue.add(it);
            Platform.runLater(() -> {
                queueSizeProperty.set(workQueue.size());
            });
        }

        @Override
        public void run() {
            // nearly infinite loop waiting for tasks
            while (true) {
                if (cancelled) {
                    return;
                }
                try {
                    // @@@ Could probably do something more fancy here and check if we've been canceled every now and then
                    InnerTask it = workQueue.take();
                    if (it.cancelled == false) {
                        it.run();
                    }
                    Platform.runLater(() -> {
                        queueSizeProperty.set(workQueue.size());
                    });

                } catch (InterruptedException ex) {
                    Exceptions.printStackTrace(ex);
                }
            }
        }
    }

    public SleuthkitCase getSleuthKitCase() throws IllegalStateException {
        if (Case.isCaseOpen()) {
            return Case.getCurrentCase().getSleuthkitCase();
        } else {
            throw new IllegalStateException("No Case is open!");
        }
    }

    /**
     * Abstract base class for task to be done on {@link DBWorkerThread}
     */
    static private abstract class InnerTask implements Runnable {

        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);
        }

        SimpleObjectProperty<Worker.State> state = new SimpleObjectProperty<>(Worker.State.READY);
        SimpleDoubleProperty progress = new SimpleDoubleProperty(this, "pregress");
        SimpleStringProperty message = new SimpleStringProperty(this, "status");

        public SimpleDoubleProperty progressProperty() {
            return progress;
        }

        public SimpleStringProperty messageProperty() {
            return message;
        }

        public Worker.State getState() {
            return state.get();
        }

        protected void updateState(Worker.State newState) {
            state.set(newState);
        }

        public ReadOnlyObjectProperty<Worker.State> stateProperty() {
            return new ReadOnlyObjectWrapper<>(state.get());
        }

        protected InnerTask() {
        }

        protected volatile boolean cancelled = false;

        public void cancel() {
            updateState(Worker.State.CANCELLED);
        }

        protected boolean isCancelled() {
            return getState() == Worker.State.CANCELLED;
        }
    }

    /**
     * Abstract base class for tasks associated with a file in the database
     */
    static private abstract class FileTask extends InnerTask {

        private final AbstractFile file;

        public AbstractFile getFile() {
            return file;
        }

        public FileTask(AbstractFile f) {
            super();
            this.file = f;
        }

    }

    /**
     * task that updates one file in database with results from ingest
     */
    private class UpdateFileTask extends FileTask {

        public UpdateFileTask(AbstractFile f) {
            super(f);
        }

        /**
         * Update a file in the database
         */
        @Override
        public void run() {
            DrawableFile<?> drawableFile = DrawableFile.create(getFile(), true);
            db.updateFile(drawableFile);
        }
    }

    /**
     * task that updates one file in database with results from ingest
     */
    private class RemoveFileTask extends FileTask {

        public RemoveFileTask(AbstractFile f) {
            super(f);
        }

        /**
         * Update a file in the database
         */
        @Override
        public void run() {
            db.removeFile(getFile().getId());
        }
    }

    /**
     * Task that runs when image analyzer listening is (re) enabled.
     *
     * Uses the presence of TSK_FILE_TYPE_SIG attributes as a approximation to
     * 'analyzed'. Grabs all files with supported image/video mime types, and
     * adds them to the Drawable DB
     */
    class CopyAnalyzedFiles extends InnerTask {

        final private String DRAWABLE_QUERY = "name LIKE '%."
                + StringUtils.join(ImageAnalyzerModule.getAllSupportedExtensions(), "' or name LIKE '%.") + "'";

        private ProgressHandle progressHandle = ProgressHandleFactory
                .createHandle("populating analyzed image/video database");

        @Override
        public void run() {
            progressHandle.start();
            updateMessage("populating analyzed image/video database");

            try {
                //grab all files with supported extension or mime types
                final List<AbstractFile> files = getSleuthKitCase().findAllFilesWhere(DRAWABLE_QUERY
                        + " or tsk_files.obj_id in (select tsk_files.obj_id from tsk_files , blackboard_artifacts,  blackboard_attributes"
                        + " where  blackboard_artifacts.obj_id = tsk_files.obj_id"
                        + " and blackboard_attributes.artifact_id = blackboard_artifacts.artifact_id"
                        + " and blackboard_artifacts.artifact_type_id = "
                        + BlackboardArtifact.ARTIFACT_TYPE.TSK_GEN_INFO.getTypeID()
                        + " and blackboard_attributes.attribute_type_id = "
                        + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_FILE_TYPE_SIG.getTypeID()
                        + " and blackboard_attributes.value_text in ('"
                        + StringUtils.join(ImageAnalyzerModule.getSupportedMimes(), "','") + "'))");
                progressHandle.switchToDeterminate(files.size());

                updateProgress(0.0);

                //do in transaction
                DrawableDB.DrawableTransaction tr = db.beginTransaction();
                int units = 0;
                for (final AbstractFile f : files) {
                    if (cancelled) {
                        LOGGER.log(Level.WARNING, "task cancelled: not all contents may be transfered to database");
                        progressHandle.finish();
                        break;
                    }
                    final Boolean hasMimeType = ImageAnalyzerModule.hasSupportedMimeType(f);
                    final boolean known = f.getKnown() == TskData.FileKnown.KNOWN;

                    if (known) {
                        db.removeFile(f.getId(), tr); //remove known files
                    } else {
                        if (hasMimeType == null) {
                            if (ImageAnalyzerModule.isSupported(f)) {
                                //no mime type but supported =>  add as not analyzed
                                db.insertFile(DrawableFile.create(f, false), tr);
                            } else {
                                //no mime type, not supported  => remove ( should never get here)
                                db.removeFile(f.getId(), tr);
                            }
                        } else {
                            if (hasMimeType) { // supported mimetype => analyzed
                                db.updateFile(DrawableFile.create(f, true), tr);
                            } else { //unsupported mimtype => analyzed but shouldn't include
                                db.removeFile(f.getId(), tr);
                            }
                        }
                    }

                    units++;
                    final int prog = units;
                    progressHandle.progress(f.getName(), units);
                    updateProgress(prog - 1 / (double) files.size());
                    updateMessage(f.getName());
                }

                progressHandle.finish();

                progressHandle = ProgressHandleFactory.createHandle("commiting image/video database");
                updateMessage("commiting image/video database");
                updateProgress(1.0);

                progressHandle.start();
                db.commitTransaction(tr, true);

            } catch (TskCoreException ex) {
                Logger.getLogger(CopyAnalyzedFiles.class.getName()).log(Level.WARNING,
                        "failed to transfer all database contents", ex);
            } catch (IllegalStateException ex) {
                Logger.getLogger(CopyAnalyzedFiles.class.getName()).log(Level.SEVERE,
                        "Case was closed out from underneath CopyDataSource task", ex);
            }

            progressHandle.finish();

            updateMessage("");
            updateProgress(-1.0);
            setStale(false);
        }

    }

    /**
     * task that does pre-ingest copy over of files from a new datasource with
     * (uses fs_obj_id to identify files from new datasource) *
     *
     * TODO: create methods to simplify progress value/text updates to both
     * netbeans and ImageAnalyzer progress/status
     */
    class PrePopulateDataSourceFiles extends InnerTask {

        private final Long id; // id of image or file
        /**
         * here we grab by extension but in file_done listener we look at file
         * type id attributes but fall back on jpeg signatures and extensions to
         * check for supported images
         */
        // (name like '.jpg' or name like '.png' ...)
        private final String DRAWABLE_QUERY = "name LIKE '%."
                + StringUtils.join(ImageAnalyzerModule.getAllSupportedExtensions(), "' or name LIKE '%.") + "'";

        private ProgressHandle progressHandle = ProgressHandleFactory
                .createHandle("prepopulating image/video database");

        public PrePopulateDataSourceFiles(Long id) {
            super();
            this.id = id;

        }

        /**
         * Copy files from a newly added data source into the DB
         */
        @Override
        public void run() {
            progressHandle.start();
            updateMessage("prepopulating image/video database");

            /* Get all "drawable" files, based on extension. After ingest we use
             * file type id module and if necessary jpeg signature matching to
             * add/remove files */
            final List<AbstractFile> files;
            try {
                files = getSleuthKitCase().findAllFilesWhere(DRAWABLE_QUERY + "and fs_obj_id = " + this.id);
                progressHandle.switchToDeterminate(files.size());

                //do in transaction
                DrawableDB.DrawableTransaction tr = db.beginTransaction();
                int units = 0;
                for (final AbstractFile f : files) {
                    if (cancelled) {
                        LOGGER.log(Level.WARNING, "task cancelled: not all contents may be transfered to database");
                        progressHandle.finish();
                        break;
                    }
                    db.insertFile(DrawableFile.create(f, false), tr);
                    units++;
                    final int prog = units;
                    progressHandle.progress(f.getName(), units);
                }

                progressHandle.finish();
                progressHandle = ProgressHandleFactory.createHandle("commiting image/video database");

                progressHandle.start();
                db.commitTransaction(tr, false);

            } catch (TskCoreException ex) {
                Logger.getLogger(PrePopulateDataSourceFiles.class.getName()).log(Level.WARNING,
                        "failed to transfer all database contents", ex);
            } catch (IllegalStateException ex) {
                Logger.getLogger(PrePopulateDataSourceFiles.class.getName()).log(Level.SEVERE,
                        "Case was closed out from underneath CopyDataSource task", ex);
            }

            progressHandle.finish();
        }
    }

}