diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/ThumbnailViewNode.java b/Core/src/org/sleuthkit/autopsy/corecomponents/ThumbnailViewNode.java index dc06466d54..5ac5b95c67 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/ThumbnailViewNode.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/ThumbnailViewNode.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011 Basis Technology Corp. + * Copyright 2011-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -95,16 +95,18 @@ class ThumbnailViewNode extends FilterNode { super.done(); try { iconCache = new SoftReference<>(super.get()); - progressHandle.finish(); fireIconChange(); + } catch (InterruptedException | ExecutionException ex) { + Logger.getLogger(ThumbnailViewNode.class.getName()).log(Level.SEVERE, "Error getting thumbnail icon", ex); //NON-NLS + } finally { + progressHandle.finish(); if (timer != null) { timer.stop(); timer = null; + } - } catch (InterruptedException | ExecutionException ex) { - Logger.getLogger(ThumbnailViewNode.class.getName()).log(Level.SEVERE, "Error getting thumbnail icon", ex); //NON-NLS + swingWorker = null; } - swingWorker = null; } }; swingWorker.execute(); diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/Bundle.properties b/Core/src/org/sleuthkit/autopsy/coreutils/Bundle.properties index 643a4b75f1..f633003d1d 100644 --- a/Core/src/org/sleuthkit/autopsy/coreutils/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/coreutils/Bundle.properties @@ -20,8 +20,4 @@ PlatformUtil.getPhysicalMemInfo.usageText=Physical memory usage (max, total, fre PlatformUtil.getAllMemUsageInfo.usageText={0}\n\ {1}\n\ Process Virtual Memory\: {2} -StringExtract.illegalStateException.cannotInit.msg=Unicode table not properly initialized, cannot instantiate StringExtract -ImageUtils.GetOrGenerateThumbnailTask.loadingThumbnailFor=Loading thumbnail for {0} -ImageUtils.GetOrGenerateThumbnailTask.generatingPreviewFor=Generating preview for {0} -ImageUtils.ReadImageTask.mesage.text=Reading image\: {0} -VideoUtils.genVideoThumb.progress.text=extracting temporary file {0} +StringExtract.illegalStateException.cannotInit.msg=Unicode table not properly initialized, cannot instantiate StringExtract \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java b/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java index be29d789c9..4e627ea09d 100755 --- a/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2012-15 Basis Technology Corp. + * Copyright 2012-16 Basis Technology Corp. * * Copyright 2012 42six Solutions. * Contact: aebadirad 42six com @@ -30,6 +30,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Paths; +import java.text.MessageFormat; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; @@ -74,10 +75,6 @@ public class ImageUtils { private static final Logger LOGGER = Logger.getLogger(ImageUtils.class.getName()); - private static final String COULD_NOT_WRITE_CACHE_THUMBNAIL = "Could not write cache thumbnail: "; //NOI18N NON-NLS - private static final String COULD_NOT_CREATE_IMAGE_INPUT_STREAM = "Could not create ImageInputStream."; //NOI18N NON-NLS - private static final String NO_IMAGE_READER_FOUND_FOR_ = "No ImageReader found for "; //NOI18N NON-NLS - /** * save thumbnails to disk as this format */ @@ -594,7 +591,7 @@ public class ImageUtils { try (InputStream inputStream = new BufferedInputStream(new ReadContentInputStream(file));) { try (ImageInputStream input = ImageIO.createImageInputStream(inputStream)) { if (input == null) { - IIOException iioException = new IIOException(COULD_NOT_CREATE_IMAGE_INPUT_STREAM); + IIOException iioException = new IIOException("Could not create ImageInputStream."); LOGGER.log(Level.WARNING, errorTemplate + iioException.toString(), getContentPathSafe(file)); throw iioException; } @@ -613,7 +610,7 @@ public class ImageUtils { reader.dispose(); } } else { - IIOException iioException = new IIOException(NO_IMAGE_READER_FOUND_FOR_ + getContentPathSafe(file)); + IIOException iioException = new IIOException("No ImageReader found."); LOGGER.log(Level.WARNING, errorTemplate + iioException.toString(), getContentPathSafe(file)); throw iioException; @@ -646,18 +643,19 @@ public class ImageUtils { */ static private class GetThumbnailTask extends ReadImageTaskBase { - private static final String FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION = "Failed to read image for thumbnail generation."; //NOI18N NON-NLS + private static final String FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION = "Failed to read {0} for thumbnail generation."; //NOI18N NON-NLS private final int iconSize; private final File cacheFile; private final boolean defaultOnFailure; -// @NbBundle.Messages({"# {0} - file name", -// "GetOrGenerateThumbnailTask.loadingThumbnailFor=Loading thumbnail for {0}", "# {0} - file name", -// "GetOrGenerateThumbnailTask.generatingPreviewFor=Generating preview for {0}"}) + @NbBundle.Messages({"# {0} - file name", + "GetOrGenerateThumbnailTask.loadingThumbnailFor=Loading thumbnail for {0}", + "# {0} - file name", + "GetOrGenerateThumbnailTask.generatingPreviewFor=Generating preview for {0}"}) private GetThumbnailTask(AbstractFile file, int iconSize, boolean defaultOnFailure) { super(file); - updateMessage(NbBundle.getMessage(this.getClass(), "ImageUtils.GetOrGenerateThumbnailTask.loadingThumbnailFor", file.getName())); + updateMessage(Bundle.GetOrGenerateThumbnailTask_loadingThumbnailFor(file.getName())); this.iconSize = iconSize; this.defaultOnFailure = defaultOnFailure; this.cacheFile = getCachedThumbnailLocation(file.getId()); @@ -678,36 +676,39 @@ public class ImageUtils { if (nonNull(cachedThumbnail) && cachedThumbnail.getWidth() == iconSize) { return SwingFXUtils.toFXImage(cachedThumbnail, null); } - } catch (IOException ex) { - LOGGER.log(Level.WARNING, "ImageIO had a problem reading thumbnail for image {0}: " + ex.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N NON-NLS + } catch (Exception ex) { + LOGGER.log(Level.WARNING, "ImageIO had a problem reading the cached thumbnail for {0}: " + ex.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N NON-NLS + cacheFile.delete(); //since we can't read the file we might as well delete it. } } if (isCancelled()) { return null; } + //There was no correctly-sized cached thumbnail so make one. BufferedImage thumbnail = null; - if (VideoUtils.isVideoThumbnailSupported(file)) { if (openCVLoaded) { - updateMessage(NbBundle.getMessage(this.getClass(), "ImageUtils.GetOrGenerateThumbnailTask.generatingPreviewFor", file.getName())); + updateMessage(Bundle.GetOrGenerateThumbnailTask_generatingPreviewFor(file.getName())); thumbnail = VideoUtils.generateVideoThumbnail(file, iconSize); } if (null == thumbnail) { if (defaultOnFailure) { thumbnail = DEFAULT_THUMBNAIL; } else { - throw new IIOException("Failed to generate thumbnail for video file."); + throw new IIOException("Failed to generate a thumbnail for " + getContentPathSafe(file)); } } } else { //read the image into a buffered image. + //TODO: I don't like this, we just converted it from BufferedIamge to fx Image -jm BufferedImage bufferedImage = SwingFXUtils.fromFXImage(readImage(), null); if (null == bufferedImage) { - LOGGER.log(Level.WARNING, FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION); - throw new IIOException(FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION); + String msg = MessageFormat.format(FAILED_TO_READ_IMAGE_FOR_THUMBNAIL_GENERATION, getContentPathSafe(file)); + LOGGER.log(Level.WARNING, msg); + throw new IIOException(msg); } updateProgress(-1, 1); @@ -716,23 +717,21 @@ public class ImageUtils { thumbnail = ScalrWrapper.resizeFast(bufferedImage, iconSize); } catch (IllegalArgumentException | OutOfMemoryError e) { // if resizing does not work due to extreme aspect ratio or oom, crop the image instead. - LOGGER.log(Level.WARNING, "Could not scale image {0}: " + e.toString() + ". Attemptying to crop {0} instead", ImageUtils.getContentPathSafe(file)); //NOI18N NON-NLS + LOGGER.log(Level.WARNING, "Cropping {0}, because it could not be scaled: " + e.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N NON-NLS final int height = bufferedImage.getHeight(); final int width = bufferedImage.getWidth(); if (iconSize < height || iconSize < width) { final int cropHeight = Math.min(iconSize, height); final int cropWidth = Math.min(iconSize, width); - try { thumbnail = ScalrWrapper.cropImage(bufferedImage, cropWidth, cropHeight); } catch (Exception cropException) { - LOGGER.log(Level.WARNING, "Could not crop image {0}: " + cropException.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N NON-NLS - throw cropException; + LOGGER.log(Level.WARNING, "Could not crop {0}: " + cropException.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N NON-NLS } } } catch (Exception e) { - LOGGER.log(Level.WARNING, "Could not scale image {0}: " + e.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N NON-NLS + LOGGER.log(Level.WARNING, "Could not scale {0}: " + e.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N NON-NLS throw e; } } @@ -744,7 +743,7 @@ public class ImageUtils { updateProgress(-1, 1); //if we got a valid thumbnail save it - if ((cacheFile != null) && nonNull(thumbnail) && DEFAULT_THUMBNAIL != thumbnail) { + if ((cacheFile != null) && thumbnail != null && DEFAULT_THUMBNAIL != thumbnail) { saveThumbnail(thumbnail); } @@ -790,16 +789,16 @@ public class ImageUtils { /** * A task that reads the content of a AbstractFile as a javafx Image. */ + @NbBundle.Messages({ + "# {0} - file name", + "ReadImageTask.mesageText=Reading image: {0}"}) static private class ReadImageTask extends ReadImageTaskBase { ReadImageTask(AbstractFile file) { super(file); - updateMessage(NbBundle.getMessage(this.getClass(), "ImageUtils.ReadImageTask.mesage.text", file.getName())); + updateMessage(Bundle.ReadImageTask_mesageText(file.getName())); } -// @NbBundle.Messages({ -// "# {0} - file name", -// "LoadImageTask.mesageText=Reading image: {0}"}) @Override protected javafx.scene.image.Image call() throws Exception { return readImage(); @@ -811,70 +810,55 @@ public class ImageUtils { */ static private abstract class ReadImageTaskBase extends Task implements IIOReadProgressListener { - private static final String IMAGE_UTILS_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT = "ImageUtils could not read {0}. It may be unsupported or corrupt"; //NOI18N NON-NLS + private static final String IMAGEIO_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT = "ImageIO could not read {0}. It may be unsupported or corrupt"; //NOI18N NON-NLS final AbstractFile file; - private ImageReader reader; +// private ImageReader reader; ReadImageTaskBase(AbstractFile file) { this.file = file; } protected javafx.scene.image.Image readImage() throws IOException { - try (InputStream inputStream = new BufferedInputStream(new ReadContentInputStream(file));) { - if (ImageUtils.isGIF(file)) { - //use JavaFX to directly read GIF to preserve potential animation, - javafx.scene.image.Image image = new javafx.scene.image.Image(new BufferedInputStream(inputStream)); - if (image.isError() == false) { - return image; - } - //fall through to default image reading code if there was an error + if (ImageUtils.isGIF(file)) { + //use JavaFX to directly read GIF to preserve potential animation + javafx.scene.image.Image image = new javafx.scene.image.Image(new BufferedInputStream(new ReadContentInputStream(file))); + if (image.isError() == false) { + return image; } - if (isCancelled()) { - return null; - } - try (ImageInputStream input = ImageIO.createImageInputStream(inputStream)) { - if (input == null) { - throw new IIOException(COULD_NOT_CREATE_IMAGE_INPUT_STREAM); - } - Iterator readers = ImageIO.getImageReaders(input); + //fall through to default image reading code if there was an error + } + if (isCancelled()) { + return null; + } - //we use the first ImageReader, is there any point to trying the others? - if (readers.hasNext()) { - reader = readers.next(); - reader.addIIOReadProgressListener(this); - reader.setInput(input); + return getImageProperty(file, "ImageIO could not read {0}: ", + imageReader -> { + imageReader.addIIOReadProgressListener(ReadImageTaskBase.this); /* * This is the important part, get or create a * ImageReadParam, create a destination image to hold * the decoded result, then pass that image with the * param. */ - ImageReadParam param = reader.getDefaultReadParam(); - - BufferedImage bufferedImage = reader.getImageTypes(0).next().createBufferedImage(reader.getWidth(0), reader.getHeight(0)); + ImageReadParam param = imageReader.getDefaultReadParam(); + BufferedImage bufferedImage = imageReader.getImageTypes(0).next().createBufferedImage(imageReader.getWidth(0), imageReader.getHeight(0)); param.setDestination(bufferedImage); try { - bufferedImage = reader.read(0, param); //should always be same bufferedImage object + bufferedImage = imageReader.read(0, param); //should always be same bufferedImage object } catch (IOException iOException) { - // Ignore this exception or display a warning or similar, for exceptions happening during decoding - LOGGER.log(Level.WARNING, IMAGE_UTILS_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT + ": " + iOException.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N + LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT + ": " + iOException.toString(), ImageUtils.getContentPathSafe(file)); //NOI18N } finally { - reader.removeIIOReadProgressListener(this); - reader.dispose(); + imageReader.removeIIOReadProgressListener(ReadImageTaskBase.this); } if (isCancelled()) { return null; } return SwingFXUtils.toFXImage(bufferedImage, null); - } else { - throw new IIOException(NO_IMAGE_READER_FOUND_FOR_ + ImageUtils.getContentPathSafe(file)); - } - } - } + }); } @Override - public void imageProgress(ImageReader source, float percentageDone) { + public void imageProgress(ImageReader reader, float percentageDone) { //update this task with the progress reported by ImageReader.read updateProgress(percentageDone, 100); if (isCancelled()) { @@ -890,11 +874,11 @@ public class ImageUtils { try { javafx.scene.image.Image fxImage = get(); if (fxImage == null) { - LOGGER.log(Level.WARNING, IMAGE_UTILS_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT, ImageUtils.getContentPathSafe(file)); + LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT, ImageUtils.getContentPathSafe(file)); } else { if (fxImage.isError()) { //if there was somekind of error, log it - LOGGER.log(Level.WARNING, IMAGE_UTILS_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT + ": " + ObjectUtils.toString(fxImage.getException()), ImageUtils.getContentPathSafe(file)); + LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT + ": " + ObjectUtils.toString(fxImage.getException()), ImageUtils.getContentPathSafe(file)); } } } catch (InterruptedException | ExecutionException ex) { @@ -905,7 +889,7 @@ public class ImageUtils { @Override protected void failed() { super.failed(); - LOGGER.log(Level.WARNING, IMAGE_UTILS_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT + ": " + ObjectUtils.toString(getException()), ImageUtils.getContentPathSafe(file)); + LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTE_OR_CORRUPT + ": " + ObjectUtils.toString(getException()), ImageUtils.getContentPathSafe(file)); } @Override @@ -950,7 +934,7 @@ public class ImageUtils { * * @return */ - private static String getContentPathSafe(Content content) { + static String getContentPathSafe(Content content) { try { return content.getUniquePath(); } catch (TskCoreException tskCoreException) { diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/VideoUtils.java b/Core/src/org/sleuthkit/autopsy/coreutils/VideoUtils.java index dc06b75019..58c61e955b 100644 --- a/Core/src/org/sleuthkit/autopsy/coreutils/VideoUtils.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/VideoUtils.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2015 Basis Technology Corp. + * Copyright 2015-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.coreutils; +import com.google.common.io.Files; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; @@ -91,33 +92,33 @@ public class VideoUtils { return isMediaThumbnailSupported(file, SUPPORTED_VIDEO_MIME_TYPES, SUPPORTED_VIDEO_EXTENSIONS, CONDITIONAL_MIME_TYPES); } + @NbBundle.Messages({"# {0} - file name", + "VideoUtils.genVideoThumb.progress.text=extracting temporary file {0}"}) static BufferedImage generateVideoThumbnail(AbstractFile file, int iconSize) { java.io.File tempFile = getTempVideoFile(file); - - try { - if (tempFile.exists() == false || tempFile.length() < file.getSize()) { - com.google.common.io.Files.createParentDirs(tempFile); - ProgressHandle progress = ProgressHandleFactory.createHandle(NbBundle.getMessage(VideoUtils.class, "VideoUtils.genVideoThumb.progress.text", file.getName())); - progress.start(100); - try { - ContentUtils.writeToFile(file, tempFile, progress, null, true); - } catch (IOException ex) { - LOGGER.log(Level.WARNING, "Error buffering file", ex); //NON-NLS - } + if (tempFile.exists() == false || tempFile.length() < file.getSize()) { + ProgressHandle progress = ProgressHandleFactory.createHandle(Bundle.VideoUtils_genVideoThumb_progress_text(file.getName())); + progress.start(100); + try { + Files.createParentDirs(tempFile); + ContentUtils.writeToFile(file, tempFile, progress, null, true); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "Error extracting temporary file for " + ImageUtils.getContentPathSafe(file), ex); //NON-NLS + } finally { progress.finish(); } - } catch (IOException ex) { - return null; } VideoCapture videoFile = new VideoCapture(); // will contain the video if (!videoFile.open(tempFile.toString())) { + LOGGER.log(Level.WARNING, "Error opening {0} for preview generation.", ImageUtils.getContentPathSafe(file)); //NON-NLS return null; } double fps = videoFile.get(CV_CAP_PROP_FPS); // gets frame per second double totalFrames = videoFile.get(CV_CAP_PROP_FRAME_COUNT); // gets total frames if (fps <= 0 || totalFrames <= 0) { + LOGGER.log(Level.WARNING, "Error getting fps or total frames for {0}", ImageUtils.getContentPathSafe(file)); //NON-NLS return null; } double milliseconds = 1000 * (totalFrames / fps); //total milliseconds @@ -132,10 +133,12 @@ public class VideoUtils { for (int x = 0; x < THUMB_COLUMNS; x++) { for (int y = 0; y < THUMB_ROWS; y++) { if (!videoFile.set(CV_CAP_PROP_POS_MSEC, timestamp + x * framkeskip + y * framkeskip * THUMB_COLUMNS)) { + LOGGER.log(Level.WARNING, "Error seeking to " + timestamp + "ms in {0}", ImageUtils.getContentPathSafe(file)); //NON-NLS break; // if we can't set the time, return black for that frame } //read the frame into the image/matrix if (!videoFile.read(imageMatrix)) { + LOGGER.log(Level.WARNING, "Error reading frames at " + timestamp + "ms from {0}", ImageUtils.getContentPathSafe(file)); //NON-NLS break; //if the image for some reason is bad, return black for that frame } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/FileTypeUtils.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/FileTypeUtils.java index 72b1eba094..806704312f 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/FileTypeUtils.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/FileTypeUtils.java @@ -107,6 +107,8 @@ public enum FileTypeUtils { , "sn", "ras" //sun raster NON-NLS , "ico" //windows icons NON-NLS , "tga" //targa NON-NLS + , "wmf", "emf" // windows meta file NON-NLS + , "wmz", "emz" //compressed windows meta file NON-NLS )); //add list of known video extensions @@ -129,6 +131,8 @@ public enum FileTypeUtils { * mime types. */ supportedMimeTypes.addAll(Arrays.asList("application/x-123")); //NON-NLS + supportedMimeTypes.addAll(Arrays.asList("application/x-wmf")); //NON-NLS + supportedMimeTypes.addAll(Arrays.asList("application/x-emf")); //NON-NLS //add list of mimetypes ImageIO claims to support supportedMimeTypes.addAll(Stream.of(ImageIO.getReaderMIMETypes()) diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java index c3aedf6dcc..1ac88ee221 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java @@ -20,6 +20,7 @@ package org.sleuthkit.autopsy.imagegallery; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -54,7 +55,6 @@ import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javax.annotation.Nullable; 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.Cancellable; @@ -85,6 +85,7 @@ import org.sleuthkit.datamodel.Image; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; +import org.sleuthkit.datamodel.VirtualDirectory; /** * Connects different parts of ImageGallery together and is hub for flow of @@ -127,8 +128,6 @@ public final class ImageGalleryController implements Executor { */ private final SimpleBooleanProperty listeningEnabled = new SimpleBooleanProperty(false); - private final ReadOnlyIntegerWrapper queueSizeProperty = new ReadOnlyIntegerWrapper(0); - private final ReadOnlyBooleanWrapper regroupDisabled = new ReadOnlyBooleanWrapper(false); @ThreadConfined(type = ThreadConfined.ThreadType.JFX) @@ -153,7 +152,6 @@ public final class ImageGalleryController implements Executor { private Node infoOverlay; private SleuthkitCase sleuthKitCase; -// private NavPanel navPanel; public ReadOnlyBooleanProperty getMetaDataCollapsed() { return metaDataCollapsed.getReadOnlyProperty(); @@ -175,7 +173,7 @@ public final class ImageGalleryController implements Executor { return historyManager.currentState(); } - public synchronized FileIDSelectionModel getSelectionModel() { + public FileIDSelectionModel getSelectionModel() { return selectionModel; } @@ -187,12 +185,16 @@ public final class ImageGalleryController implements Executor { return db; } - synchronized public void setListeningEnabled(boolean enabled) { - listeningEnabled.set(enabled); + public void setListeningEnabled(boolean enabled) { + synchronized (listeningEnabled) { + listeningEnabled.set(enabled); + } } - synchronized boolean isListeningEnabled() { - return listeningEnabled.get(); + boolean isListeningEnabled() { + synchronized (listeningEnabled) { + return listeningEnabled.get(); + } } @ThreadConfined(type = ThreadConfined.ThreadType.ANY) @@ -248,12 +250,14 @@ public final class ImageGalleryController implements Executor { checkForGroups(); }); - IngestManager.getInstance().addIngestModuleEventListener((PropertyChangeEvent evt) -> { - Platform.runLater(this::updateRegroupDisabled); - }); - IngestManager.getInstance().addIngestJobEventListener((PropertyChangeEvent evt) -> { - Platform.runLater(this::updateRegroupDisabled); - }); + 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() { @@ -280,8 +284,9 @@ public final class ImageGalleryController implements Executor { return historyManager.retreat(); } + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) private void updateRegroupDisabled() { - regroupDisabled.set(getFileUpdateQueueSizeProperty().get() > 0 || IngestManager.getInstance().isIngestRunning()); + regroupDisabled.set((queueSizeProperty.get() > 0) || IngestManager.getInstance().isIngestRunning()); } /** @@ -290,16 +295,16 @@ public final class ImageGalleryController implements Executor { * 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."}) + @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()) { @@ -312,7 +317,7 @@ public final class ImageGalleryController implements Executor { new ProgressIndicator())); } - } else if (getFileUpdateQueueSizeProperty().get() > 0) { + } else if (queueSizeProperty.get() > 0) { replaceNotification(fullUIStackPane, new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg3(), new ProgressIndicator())); @@ -357,20 +362,14 @@ public final class ImageGalleryController implements Executor { } } - private void restartWorker() { - if (dbWorkerThread != null) { + 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 = new DBWorkerThread(); - - getFileUpdateQueueSizeProperty().addListener((Observable o) -> { - Platform.runLater(this::updateRegroupDisabled); - }); - - Thread th = new Thread(dbWorkerThread, "DB-Worker-Thread"); - th.setDaemon(false); // we want it to go away when it is done - th.start(); + return dbWorkerThread; } /** @@ -411,15 +410,16 @@ public final class ImageGalleryController implements Executor { setListeningEnabled(false); ThumbnailCache.getDefault().clearCache(); historyManager.clear(); + groupManager.clear(); tagsManager.clearFollowUpTagName(); tagsManager.unregisterListener(groupManager); tagsManager.unregisterListener(categoryManager); - dbWorkerThread.cancelAllTasks(); + dbWorkerThread.cancel(); dbWorkerThread = null; - restartWorker(); + dbWorkerThread = restartWorker(); Toolbar.getDefault(this).reset(); - groupManager.clear(); + if (db != null) { db.closeDBCon(); } @@ -431,11 +431,9 @@ public final class ImageGalleryController implements Executor { * * @param innerTask */ - public void queueDBWorkerTask(InnerTask innerTask) { - - // @@@ We could make a lock for the worker thread + public synchronized void queueDBWorkerTask(BackgroundTask innerTask) { if (dbWorkerThread == null) { - restartWorker(); + dbWorkerThread = restartWorker(); } dbWorkerThread.addTask(innerTask); } @@ -455,10 +453,6 @@ public final class ImageGalleryController implements Executor { Platform.runLater(this::checkForGroups); } - public ReadOnlyIntegerProperty getFileUpdateQueueSizeProperty() { - return queueSizeProperty.getReadOnlyProperty(); - } - public ReadOnlyDoubleProperty regroupProgress() { return groupManager.regroupProgress(); } @@ -496,29 +490,43 @@ public final class ImageGalleryController implements Executor { return undoManager; } - // @@@ REVIEW IF THIS SHOLD BE STATIC... - //TODO: concept seems like the controller deal with how much work to do at a given time + 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.) - private class DBWorkerThread implements Runnable { + 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 workQueue = new LinkedBlockingQueue<>(); + private final BlockingQueue 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() { + @Override + public boolean cancel() { cancelled = true; - for (InnerTask it : workQueue) { + for (BackgroundTask it : workQueue) { it.cancel(); } workQueue.clear(); - queueSizeProperty.set(workQueue.size()); + int size = workQueue.size(); + Platform.runLater(() -> controller.queueSizeProperty.set(size)); + return true; } /** @@ -526,11 +534,10 @@ public final class ImageGalleryController implements Executor { * * @param it */ - public void addTask(InnerTask it) { + public void addTask(BackgroundTask it) { workQueue.add(it); - Platform.runLater(() -> { - queueSizeProperty.set(workQueue.size()); - }); + int size = workQueue.size(); + Platform.runLater(() -> controller.queueSizeProperty.set(size)); } @Override @@ -538,19 +545,17 @@ public final class ImageGalleryController implements Executor { // nearly infinite loop waiting for tasks while (true) { - if (cancelled) { + if (cancelled || isInterrupted()) { return; } try { - InnerTask it = workQueue.take(); + BackgroundTask it = workQueue.take(); if (it.isCancelled() == false) { it.run(); } - - Platform.runLater(() -> { - queueSizeProperty.set(workQueue.size()); - }); + 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 @@ -567,8 +572,15 @@ public final class ImageGalleryController implements Executor { * 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 InnerTask implements Runnable, Cancellable { + "ImageGalleryController.InnerTask.message.name=status"}) + static public abstract class BackgroundTask implements Runnable, Cancellable { + + private final SimpleObjectProperty 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(); @@ -585,9 +597,6 @@ public final class ImageGalleryController implements Executor { public final void updateMessage(String Status) { this.message.set(Status); } - SimpleObjectProperty state = new SimpleObjectProperty<>(Worker.State.READY); - SimpleDoubleProperty progress = new SimpleDoubleProperty(this, Bundle.ImageGalleryController_InnerTask_progress_name()); - SimpleStringProperty message = new SimpleStringProperty(this, Bundle.ImageGalleryController_InnerTask_message_name()); public SimpleDoubleProperty progressProperty() { return progress; @@ -601,24 +610,21 @@ public final class ImageGalleryController implements Executor { return state.get(); } - protected void updateState(Worker.State newState) { - state.set(newState); - } - public ReadOnlyObjectProperty stateProperty() { return new ReadOnlyObjectWrapper<>(state.get()); } - protected InnerTask() { - } - @Override - synchronized public boolean cancel() { + public synchronized boolean cancel() { updateState(Worker.State.CANCELLED); return true; } - synchronized protected boolean isCancelled() { + protected void updateState(Worker.State newState) { + state.set(newState); + } + + protected synchronized boolean isCancelled() { return getState() == Worker.State.CANCELLED; } } @@ -626,7 +632,7 @@ public final class ImageGalleryController implements Executor { /** * Abstract base class for tasks associated with a file in the database */ - static public abstract class FileTask extends InnerTask { + static public abstract class FileTask extends BackgroundTask { private final AbstractFile file; private final DrawableDB taskDB; @@ -644,7 +650,6 @@ public final class ImageGalleryController implements Executor { this.file = f; this.taskDB = taskDB; } - } /** @@ -701,63 +706,63 @@ public final class ImageGalleryController implements Executor { } } - /** - * 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.populatingDb.status=populating analyzed image/video database", - "CopyAnalyzedFiles.committingDb.status=commiting 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 InnerTask { + @NbBundle.Messages({"BulkTask.committingDb.status=commiting 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 { - private final ImageGalleryController controller; - private final DrawableDB taskDB; - private final SleuthkitCase tskCase; + static private final String FILE_EXTENSION_CLAUSE = + "(name LIKE '%." //NON-NLS + + String.join("' OR name LIKE '%.", FileTypeUtils.getAllSupportedExtensions()) //NON-NLS + + "')"; - CopyAnalyzedFiles(ImageGalleryController controller, DrawableDB taskDB, SleuthkitCase tskCase) { + 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; } - static private final String FILE_EXTENSION_CLAUSE = - "(name LIKE '%." //NON-NLS - + StringUtils.join(FileTypeUtils.getAllSupportedExtensions(), "' OR name LIKE '%.") //NON-NLS - + "')"; + abstract void cleanup(boolean success); - static private final String MIMETYPE_CLAUSE = - "(mime_type LIKE '" //NON-NLS - + StringUtils.join(FileTypeUtils.getAllSupportedMimeTypes(), "' OR mime_type LIKE '") //NON-NLS - + "') "; + abstract List getFiles() throws TskCoreException; - static private final String DRAWABLE_QUERY = - //grab files with supported extension - FILE_EXTENSION_CLAUSE - //grab files with supported mime-types - + " OR " + MIMETYPE_CLAUSE //NON-NLS - //grab files with image or video mime-types even if we don't officially support them - + " OR mime_type LIKE 'video/%' OR mime_type LIKE 'image/%'"; //NON-NLS - private ProgressHandle progressHandle = ProgressHandleFactory.createHandle(Bundle.CopyAnalyzedFiles_populatingDb_status()); + 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 files = tskCase.findAllFilesWhere(DRAWABLE_QUERY); + final List files = getFiles(); progressHandle.switchToDeterminate(files.size()); updateProgress(0.0); //do in transaction DrawableDB.DrawableTransaction tr = taskDB.beginTransaction(); - int units = 0; + 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 @@ -765,161 +770,172 @@ public final class ImageGalleryController implements Executor { break; } - final boolean known = f.getKnown() == TskData.FileKnown.KNOWN; + processFile(f, tr); - if (known) { - taskDB.removeFile(f.getId(), tr); //remove known files - } else { - Optional mimeType = FileTypeUtils.getMimeType(f); - if (mimeType.isPresent()) { - //mime type - if (FileTypeUtils.isDrawableMimeType(mimeType.get())) { //supported mimetype => analyzed - taskDB.updateFile(DrawableFile.create(f, true, false), tr); - } else { //unsupported mimtype => analyzed but shouldn't include - taskDB.removeFile(f.getId(), tr); - } - } else { - //no mime tyoe - if (FileTypeUtils.isDrawable(f)) { - //no mime type but supported => add as not analyzed - taskDB.insertFile(DrawableFile.create(f, false, false), tr); - } else { - //no mime type, not supported => remove ( should never get here) - taskDB.removeFile(f.getId(), tr); - } - } - } - - units++; - final int prog = units; - progressHandle.progress(f.getName(), units); - updateProgress(prog - 1 / (double) files.size()); + workDone++; + progressHandle.progress(f.getName(), workDone); + updateProgress(workDone - 1 / (double) files.size()); updateMessage(f.getName()); } progressHandle.finish(); - - progressHandle = ProgressHandleFactory.createHandle(Bundle.CopyAnalyzedFiles_committingDb_status()); - updateMessage(Bundle.CopyAnalyzedFiles_committingDb_status()); + progressHandle = ProgressHandleFactory.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.CopyAnalyzedFiles_stopCopy_status()); - Logger.getLogger(CopyAnalyzedFiles.class.getName()).log(Level.WARNING, "Stopping copy to drawable db task. Failed to transfer all database contents: " + ex.getMessage()); //NON-NLS - MessageNotifyUtil.Notify.warn(Bundle.CopyAnalyzedFiles_errPopulating_errMsg(), ex.getMessage()); + 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); - controller.setStale(true); - return; } + cleanup(true); + } - progressHandle.finish(); - updateMessage(""); - updateProgress(-1.0); - controller.setStale(false); + 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=commiting 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 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 { + Optional mimeType = FileTypeUtils.getMimeType(f); + if (mimeType.isPresent()) { + //mime type + if (FileTypeUtils.isDrawableMimeType(mimeType.get())) { //supported mimetype => analyzed + taskDB.updateFile(DrawableFile.create(f, true, false), tr); + } else { //unsupported mimtype => analyzed but shouldn't include + taskDB.removeFile(f.getId(), tr); + } + } else { + //no mime tyoe + if (FileTypeUtils.isDrawable(f)) { + //no mime type but supported => add as not analyzed + taskDB.insertFile(DrawableFile.create(f, false, false), tr); + } else { + //no mime type, not supported => remove ( should never get here) + taskDB.removeFile(f.getId(), tr); + } + } + } + } + + @Override + @NbBundle.Messages({"CopyAnalyzedFiles.populatingDb.status=populating analyzed image/video database",}) + ProgressHandle getInitialProgressHandle() { + return ProgressHandleFactory.createHandle(Bundle.CopyAnalyzedFiles_populatingDb_status(), this); } } /** - * task that does pre-ingest copy over of files from a new datasource (uses - * fs_obj_id to identify files from new datasources) + * 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.prepopulatingDb.status=prepopulating image/video database", - "PrePopulateDataSourceFiles.committingDb.status=commiting image/video database"}) - private class PrePopulateDataSourceFiles extends InnerTask { + @NbBundle.Messages({"PrePopulateDataSourceFiles.committingDb.status=commiting image/video database"}) + static private class PrePopulateDataSourceFiles extends BulkTransferTask { + + private static final Logger LOGGER = Logger.getLogger(PrePopulateDataSourceFiles.class.getName()); private final Content dataSource; - /** - * 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(FileTypeUtils.getAllSupportedExtensions(), "' OR name LIKE '%.") + "') "; //NON-NLS - - private ProgressHandle progressHandle = ProgressHandleFactory.createHandle(Bundle.PrePopulateDataSourceFiles_prepopulatingDb_status(), this); - /** * * @param dataSourceId Data source object ID */ - PrePopulateDataSourceFiles(Content dataSource) { - super(); + PrePopulateDataSourceFiles(Content dataSource, ImageGalleryController controller, DrawableDB taskDB, SleuthkitCase tskCase) { + super(controller, taskDB, tskCase); this.dataSource = dataSource; } - /** - * Copy files from a newly added data source into the DB. Get all - * "drawable" files, based on extension. After ingest we use file type - * id module and if necessary jpeg/png signature matching to add/remove - * files - */ @Override - public void run() { - progressHandle.start(); - updateMessage(Bundle.PrePopulateDataSourceFiles_prepopulatingDb_status()); + protected void cleanup(boolean success) { + } - try { - String fsQuery = "(fs_obj_id IS NULL) "; //default clause NON-NLS + @Override + void processFile(final AbstractFile f, DrawableDB.DrawableTransaction tr) { + taskDB.insertFile(DrawableFile.create(f, false, false), tr); + } + + @Override + List getFiles() throws TskCoreException { + if (dataSource instanceof Image) { + List fileSystems = ((Image) dataSource).getFileSystems(); + if (fileSystems.isEmpty()) { + /* + * no filesystems, don't bother with the initial population, + * just sort things out on file_done events + */ + progressHandle.finish(); + return Collections.emptyList(); + } + //use this clause to only grab files from the newly added filesystems. + String fsQuery = fileSystems.stream() + .map(fileSystem -> String.valueOf(fileSystem.getId())) + .collect(Collectors.joining(" OR fs_obj_id = ", "(fs_obj_id = ", ") ")); //NON-NLS + + return tskCase.findAllFilesWhere(fsQuery + " AND " + DRAWABLE_QUERY); //NON-NLS + } else if (dataSource instanceof VirtualDirectory) { /* - * NOTE: Logical files currently (Apr '15) have a null value for - * fs_obj_id in DB. for them, we will not specify a fs_obj_id, - * which means we will grab files from another data source, but - * the drawable DB is smart enough to de-dupe them. For Images - * we can do better. + * fs_obj_id is set only for file system files, so we will match + * the VirtualDirectory's name in the parent path. + * + * TODO: A future database schema could probably make this + * cleaner. If we had a datasource_id column in the files table + * we could just match agains that. */ - if (dataSource instanceof Image) { - List fileSystems = ((Image) dataSource).getFileSystems(); - if (fileSystems.isEmpty()) { - /* - * no filesystems, don't bother with the initial - * population, just sort things out on file_done events - */ - progressHandle.finish(); - return; - } - //use this clause to only grab files from the newly added filesystems. - fsQuery = fileSystems.stream() - .map(fileSystem -> String.valueOf(fileSystem.getId())) - .collect(Collectors.joining(" OR fs_obj_id = ", "(fs_obj_id = ", ") ")); //NON-NLS - } - - final List files = getSleuthKitCase().findAllFilesWhere(fsQuery + " AND " + DRAWABLE_QUERY); //NON-NLS - progressHandle.switchToDeterminate(files.size()); - - //do in transaction - DrawableDB.DrawableTransaction tr = db.beginTransaction(); - int units = 0; - for (final AbstractFile f : files) { - if (isCancelled()) { - LOGGER.log(Level.WARNING, "task cancelled: not all contents may be transfered to database"); //NON-NLS - progressHandle.finish(); - break; - } - db.insertFile(DrawableFile.create(f, false, false), tr); - units++; - progressHandle.progress(f.getName(), units); - } - - progressHandle.finish(); - progressHandle = ProgressHandleFactory.createHandle(Bundle.PrePopulateDataSourceFiles_committingDb_status()); - - progressHandle.start(); - db.commitTransaction(tr, false); - - } catch (TskCoreException ex) { - Logger.getLogger(PrePopulateDataSourceFiles.class.getName()).log(Level.WARNING, "failed to transfer all database contents", ex); //NON-NLS + return tskCase.findAllFilesWhere(" parent_path LIKE '/" + dataSource.getName() + "/%' AND " + DRAWABLE_QUERY); //NON-NLS + } else { + String msg = "Uknown datasource type: " + dataSource.getClass().getName(); + LOGGER.log(Level.SEVERE, msg); + throw new IllegalArgumentException(msg); } + } - progressHandle.finish(); + @Override + @NbBundle.Messages({"PrePopulateDataSourceFiles.prepopulatingDb.status=prepopulating image/video database",}) + ProgressHandle getInitialProgressHandle() { + return ProgressHandleFactory.createHandle(Bundle.PrePopulateDataSourceFiles_prepopulatingDb_status(), this); } } @@ -1011,7 +1027,7 @@ public final class ImageGalleryController implements Executor { //copy all file data to drawable databse Content newDataSource = (Content) evt.getNewValue(); if (isListeningEnabled()) { - queueDBWorkerTask(new PrePopulateDataSourceFiles(newDataSource)); + queueDBWorkerTask(new PrePopulateDataSourceFiles(newDataSource, ImageGalleryController.this, getDatabase(), getSleuthKitCase())); } else {//TODO: keep track of what we missed for later setStale(true); } @@ -1028,7 +1044,6 @@ public final class ImageGalleryController implements Executor { getTagsManager().fireTagDeletedEvent(tagDeletedEvent); } break; - } } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryTopComponent.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryTopComponent.java index e927312ee3..0ccf7542f3 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryTopComponent.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryTopComponent.java @@ -157,14 +157,15 @@ public final class ImageGalleryTopComponent extends TopComponent implements Expl hashHitList = new HashHitGroupList(controller); TabPane tabPane = new TabPane(groupTree, hashHitList); - + tabPane.setPrefWidth(TabPane.USE_COMPUTED_SIZE); + tabPane.setMinWidth(TabPane.USE_PREF_SIZE); VBox.setVgrow(tabPane, Priority.ALWAYS); leftPane = new VBox(tabPane, new SummaryTablePane(controller)); SplitPane.setResizableWithParent(leftPane, Boolean.FALSE); SplitPane.setResizableWithParent(groupPane, Boolean.TRUE); SplitPane.setResizableWithParent(metaDataTable, Boolean.FALSE); splitPane.getItems().addAll(leftPane, centralStack, metaDataTable); - splitPane.setDividerPositions(0.0, 1.0); + splitPane.setDividerPositions(0.1, 1.0); ImageGalleryController.getDefault().setStacks(fullUIStack, centralStack); ImageGalleryController.getDefault().setShowTree(() -> tabPane.getSelectionModel().select(groupTree)); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddDrawableTagAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddDrawableTagAction.java deleted file mode 100644 index 92e9a979c2..0000000000 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddDrawableTagAction.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2013-15 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.actions; - -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ExecutionException; -import java.util.logging.Level; -import javafx.application.Platform; -import javafx.scene.control.Alert; -import javafx.scene.control.Menu; -import javax.swing.SwingWorker; - -import org.openide.util.NbBundle; -import org.openide.util.Utilities; -import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; -import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile; -import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableTagsManager; -import org.sleuthkit.datamodel.AbstractFile; -import org.sleuthkit.datamodel.ContentTag; -import org.sleuthkit.datamodel.TagName; -import org.sleuthkit.datamodel.TskCoreException; - -/** - * Instances of this Action allow users to apply tags to content. - * - * //TODO: since we are not using actionsGlobalContext anymore and this has - * diverged from autopsy action, make this extend from controlsfx Action - */ -public class AddDrawableTagAction extends AddTagAction { - - private static final Logger LOGGER = Logger.getLogger(AddDrawableTagAction.class.getName()); - - private final ImageGalleryController controller; - - public AddDrawableTagAction(ImageGalleryController controller) { - this.controller = controller; - } - - public Menu getPopupMenu() { - return new TagMenu(controller); - } - - @Override - @NbBundle.Messages({"AddDrawableTagAction.displayName.plural=Tag Files", - "AddDrawableTagAction.displayName.singular=Tag File"}) - protected String getActionDisplayName() { - return Utilities.actionsGlobalContext().lookupAll(AbstractFile.class).size() > 1 - ? Bundle.AddDrawableTagAction_displayName_plural() - : Bundle.AddDrawableTagAction_displayName_singular(); - } - - @Override - public void addTag(TagName tagName, String comment) { - Set selectedFiles = new HashSet<>(controller.getSelectionModel().getSelected()); - addTagsToFiles(tagName, comment, selectedFiles); - } - - @Override - @NbBundle.Messages({"# {0} - fileID", - "AddDrawableTagAction.addTagsToFiles.alert=Unable to tag file {0}."}) - public void addTagsToFiles(TagName tagName, String comment, Set selectedFiles) { - new SwingWorker() { - - @Override - protected Void doInBackground() throws Exception { - for (Long fileID : selectedFiles) { - try { - final DrawableFile file = controller.getFileFromId(fileID); - LOGGER.log(Level.INFO, "tagging {0} with {1} and comment {2}", new Object[]{file.getName(), tagName.getDisplayName(), comment}); //NON-NLS - - // check if the same tag is being added for the same abstract file. - DrawableTagsManager tagsManager = controller.getTagsManager(); - List contentTags = tagsManager.getContentTagsByContent(file); - Optional duplicateTagName = contentTags.stream() - .map(ContentTag::getName) - .filter(tagName::equals) - .findAny(); - - if (duplicateTagName.isPresent()) { - LOGGER.log(Level.INFO, "{0} already tagged as {1}. Skipping.", new Object[]{file.getName(), tagName.getDisplayName()}); //NON-NLS - } else { - LOGGER.log(Level.INFO, "Tagging {0} as {1}", new Object[]{file.getName(), tagName.getDisplayName()}); //NON-NLS - controller.getTagsManager().addContentTag(file, tagName, comment); - } - - } catch (TskCoreException tskCoreException) { - LOGGER.log(Level.SEVERE, "Error tagging file", tskCoreException); //NON-NLS - Platform.runLater(() -> { - new Alert(Alert.AlertType.ERROR, Bundle.AddDrawableTagAction_addTagsToFiles_alert(fileID)).show(); - }); - } - } - return null; - } - - @Override - protected void done() { - super.done(); - try { - get(); - } catch (InterruptedException | ExecutionException ex) { - LOGGER.log(Level.SEVERE, "unexpected exception while tagging files", ex); //NON-NLS - } - } - }.execute(); - } -} diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddTagAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddTagAction.java index 7742ec583c..72600d2e5d 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddTagAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddTagAction.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013 Basis Technology Corp. + * Copyright 2013-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,67 +20,128 @@ package org.sleuthkit.autopsy.imagegallery.actions; import java.awt.Window; import java.util.Collection; +import java.util.List; +import java.util.Optional; import java.util.Set; -import javafx.event.ActionEvent; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import javafx.application.Platform; +import javafx.collections.ObservableSet; +import javafx.scene.control.Alert; import javafx.scene.control.Menu; import javafx.scene.control.MenuItem; +import javafx.scene.image.ImageView; import javax.swing.SwingUtilities; - +import javax.swing.SwingWorker; +import org.controlsfx.control.action.Action; +import org.controlsfx.control.action.ActionUtils; import org.openide.util.NbBundle; import org.openide.windows.TopComponent; import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.actions.GetTagNameAndCommentDialog; import org.sleuthkit.autopsy.actions.GetTagNameDialog; +import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.ImageGalleryTopComponent; -import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager; +import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute; +import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile; +import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableTagsManager; +import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.TagName; +import org.sleuthkit.datamodel.TskCoreException; /** - * An abstract base class for actions that allow users to tag SleuthKit data - * model objects. - * - * //TODO: this class started as a cut and paste from - * org.sleuthkit.autopsy.actions.AddTagAction and needs to be refactored or - * reintegrated to the AddTagAction hierarchy of Autopysy. + * Instances of this Action allow users to apply tags to content. */ -abstract class AddTagAction { +public class AddTagAction extends Action { - protected static final String NO_COMMENT = ""; + private static final Logger LOGGER = Logger.getLogger(AddTagAction.class.getName()); - /** - * Template method to allow derived classes to provide a string for a menu - * item label. - */ - abstract protected String getActionDisplayName(); + private final ImageGalleryController controller; + private final Set selectedFileIDs; + private final TagName tagName; - /** - * Template method to allow derived classes to add the indicated tag and - * comment to one or more a SleuthKit data model objects. - */ - abstract protected void addTag(TagName tagName, String comment); + public AddTagAction(ImageGalleryController controller, TagName tagName, Set selectedFileIDs) { + super(tagName.getDisplayName()); + this.controller = controller; + this.selectedFileIDs = selectedFileIDs; + this.tagName = tagName; + setGraphic(controller.getTagsManager().getGraphic(tagName)); + setText(tagName.getDisplayName()); + setEventHandler(actionEvent -> addTagWithComment("")); + } - /** - * Template method to allow derived classes to add the indicated tag and - * comment to a list of one or more file IDs. - */ - abstract protected void addTagsToFiles(TagName tagName, String comment, Set selectedFiles); + static public Menu getTagMenu(ImageGalleryController controller) { + return new TagMenu(controller); + } + + private void addTagWithComment(String comment) { + addTagsToFiles(tagName, comment, selectedFileIDs); + } + + @NbBundle.Messages({"# {0} - fileID", + "AddDrawableTagAction.addTagsToFiles.alert=Unable to tag file {0}."}) + private void addTagsToFiles(TagName tagName, String comment, Set selectedFiles) { + new SwingWorker() { + + @Override + protected Void doInBackground() throws Exception { + // check if the same tag is being added for the same abstract file. + DrawableTagsManager tagsManager = controller.getTagsManager(); + for (Long fileID : selectedFiles) { + try { + final DrawableFile file = controller.getFileFromId(fileID); + LOGGER.log(Level.INFO, "tagging {0} with {1} and comment {2}", new Object[]{file.getName(), tagName.getDisplayName(), comment}); //NON-NLS + + List contentTags = tagsManager.getContentTagsByContent(file); + Optional duplicateTagName = contentTags.stream() + .map(ContentTag::getName) + .filter(tagName::equals) + .findAny(); + + if (duplicateTagName.isPresent()) { + LOGGER.log(Level.INFO, "{0} already tagged as {1}. Skipping.", new Object[]{file.getName(), tagName.getDisplayName()}); //NON-NLS + } else { + LOGGER.log(Level.INFO, "Tagging {0} as {1}", new Object[]{file.getName(), tagName.getDisplayName()}); //NON-NLS + controller.getTagsManager().addContentTag(file, tagName, comment); + } + + } catch (TskCoreException tskCoreException) { + LOGGER.log(Level.SEVERE, "Error tagging file", tskCoreException); //NON-NLS + Platform.runLater(() -> + new Alert(Alert.AlertType.ERROR, Bundle.AddDrawableTagAction_addTagsToFiles_alert(fileID)).show() + ); + } + } + return null; + } + + @Override + protected void done() { + super.done(); + try { + get(); + } catch (InterruptedException | ExecutionException ex) { + LOGGER.log(Level.SEVERE, "unexpected exception while tagging files", ex); //NON-NLS + } + } + }.execute(); + } - /** - * Instances of this class implement a context menu user interface for - * creating or selecting a tag name for a tag and specifying an optional tag - * comment. - */ - // @@@ This user interface has some significant usability issues and needs - // to be reworked. @NbBundle.Messages({"AddTagAction.menuItem.quickTag=Quick Tag", - "AddTagAction.menuItem.noTags=No tags", - "AddTagAction.menuItem.newTag=New Tag...", - "AddTagAction.menuItem.tagAndComment=Tag and Comment..."}) - protected class TagMenu extends Menu { + "AddTagAction.menuItem.noTags=No tags", + "AddTagAction.menuItem.newTag=New Tag...", + "AddTagAction.menuItem.tagAndComment=Tag and Comment...", + "AddDrawableTagAction.displayName.plural=Tag Files", + "AddDrawableTagAction.displayName.singular=Tag File"}) + private static class TagMenu extends Menu { TagMenu(ImageGalleryController controller) { - super(getActionDisplayName()); + setGraphic(new ImageView(DrawableAttribute.TAGS.getIcon())); + ObservableSet selectedFileIDs = controller.getSelectionModel().getSelected(); + setText(selectedFileIDs.size() > 1 + ? Bundle.AddDrawableTagAction_displayName_plural() + : Bundle.AddDrawableTagAction_displayName_singular()); // Create a "Quick Tag" sub-menu. Menu quickTagMenu = new Menu(Bundle.AddTagAction_menuItem_quickTag()); @@ -98,10 +159,8 @@ abstract class AddTagAction { quickTagMenu.getItems().add(empty); } else { for (final TagName tagName : tagNames) { - MenuItem tagNameItem = new MenuItem(tagName.getDisplayName()); - tagNameItem.setOnAction((ActionEvent t) -> { - addTag(tagName, NO_COMMENT); - }); + AddTagAction addDrawableTagAction = new AddTagAction(controller, tagName, selectedFileIDs); + MenuItem tagNameItem = ActionUtils.createMenuItem(addDrawableTagAction); quickTagMenu.getItems().add(tagNameItem); } } @@ -112,14 +171,13 @@ abstract class AddTagAction { * or select a tag name and adds a tag with the resulting name. */ MenuItem newTagMenuItem = new MenuItem(Bundle.AddTagAction_menuItem_newTag()); - newTagMenuItem.setOnAction((ActionEvent t) -> { - SwingUtilities.invokeLater(() -> { - TagName tagName = GetTagNameDialog.doDialog(getIGWindow()); - if (tagName != null) { - addTag(tagName, NO_COMMENT); - } - }); - }); + newTagMenuItem.setOnAction(actionEvent -> + SwingUtilities.invokeLater(() -> { + TagName tagName = GetTagNameDialog.doDialog(getIGWindow()); + if (tagName != null) { + new AddTagAction(controller, tagName, selectedFileIDs).handle(actionEvent); + } + })); quickTagMenu.getItems().add(newTagMenuItem); /* @@ -129,26 +187,17 @@ abstract class AddTagAction { * name. */ MenuItem tagAndCommentItem = new MenuItem(Bundle.AddTagAction_menuItem_tagAndComment()); - tagAndCommentItem.setOnAction((ActionEvent t) -> { - SwingUtilities.invokeLater(() -> { - GetTagNameAndCommentDialog.TagNameAndComment tagNameAndComment = GetTagNameAndCommentDialog.doDialog(getIGWindow()); - if (null != tagNameAndComment) { - if (CategoryManager.isCategoryTagName(tagNameAndComment.getTagName())) { - new CategorizeAction(controller).addTag(tagNameAndComment.getTagName(), tagNameAndComment.getComment()); - } else { - new AddDrawableTagAction(controller).addTag(tagNameAndComment.getTagName(), tagNameAndComment.getComment()); + tagAndCommentItem.setOnAction(actionEvent -> + SwingUtilities.invokeLater(() -> { + GetTagNameAndCommentDialog.TagNameAndComment tagNameAndComment = GetTagNameAndCommentDialog.doDialog(getIGWindow()); + if (null != tagNameAndComment) { + new AddTagAction(controller, tagNameAndComment.getTagName(), selectedFileIDs).addTagWithComment(tagNameAndComment.getComment()); } - } - }); - }); + })); getItems().add(tagAndCommentItem); } - } - /** - * @return the Window containing the ImageGalleryTopComponent - */ static private Window getIGWindow() { TopComponent etc = WindowManager.getDefault().findTopComponent(ImageGalleryTopComponent.PREFERRED_ID); return SwingUtilities.getWindowAncestor(etc); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeAction.java index 8c819373c8..77d3f2c0f9 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeAction.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013 Basis Technology Corp. + * Copyright 2013-16 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,26 +21,28 @@ package org.sleuthkit.autopsy.imagegallery.actions; import com.google.common.collect.ImmutableMap; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.stream.Collectors; -import javafx.event.ActionEvent; +import javafx.collections.ObservableSet; import javafx.scene.control.Menu; import javafx.scene.control.MenuItem; +import javafx.scene.image.ImageView; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javax.annotation.Nonnull; import javax.annotation.concurrent.Immutable; import javax.swing.JOptionPane; - +import org.controlsfx.control.action.Action; +import org.controlsfx.control.action.ActionUtils; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.datamodel.Category; import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager; +import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableTagsManager; import org.sleuthkit.datamodel.ContentTag; @@ -49,48 +51,42 @@ import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.TskCoreException; /** - * Adaptation of Tag Actions to enforce category-tag uniqueness * - * TODO: since we are not using actionsGlobalContext anymore and this has - * diverged from autopsy action, make this extend from controlsfx Action */ @NbBundle.Messages({"CategorizeAction.displayName=Categorize"}) -public class CategorizeAction extends AddTagAction { +public class CategorizeAction extends Action { private static final Logger LOGGER = Logger.getLogger(CategorizeAction.class.getName()); private final ImageGalleryController controller; private final UndoRedoManager undoManager; + private final Category cat; + private final Set selectedFileIDs; + private final Boolean createUndo; - public CategorizeAction(ImageGalleryController controller) { - super(); - this.controller = controller; - undoManager = controller.getUndoManager(); + public CategorizeAction(ImageGalleryController controller, Category cat, Set selectedFileIDs) { + this(controller, cat, selectedFileIDs, true); } - public Menu getPopupMenu() { + private CategorizeAction(ImageGalleryController controller, Category cat, Set selectedFileIDs, Boolean createUndo) { + super(cat.getDisplayName()); + this.controller = controller; + this.undoManager = controller.getUndoManager(); + this.cat = cat; + this.selectedFileIDs = selectedFileIDs; + this.createUndo = createUndo; + setGraphic(cat.getGraphic()); + setEventHandler(actionEvent -> addCatToFiles()); + setAccelerator(new KeyCodeCombination(KeyCode.getKeyCode(Integer.toString(cat.getCategoryNumber())))); + } + + static public Menu getCategoriesMenu(ImageGalleryController controller) { return new CategoryMenu(controller); } - @Override - protected String getActionDisplayName() { - return Bundle.CategorizeAction_displayName(); - } - - @Override - public void addTag(TagName tagName, String comment) { - Set selectedFiles = new HashSet<>(controller.getSelectionModel().getSelected()); - addTagsToFiles(tagName, comment, selectedFiles); - } - - @Override - protected void addTagsToFiles(TagName tagName, String comment, Set selectedFiles) { - addTagsToFiles(tagName, comment, selectedFiles, true); - } - - public void addTagsToFiles(TagName tagName, String comment, Set selectedFiles, boolean createUndo) { - Logger.getAnonymousLogger().log(Level.INFO, "categorizing{0} as {1}", new Object[]{selectedFiles.toString(), tagName.getDisplayName()}); //NON-NLS - controller.queueDBWorkerTask(new CategorizeTask(selectedFiles, tagName, comment, createUndo)); + private void addCatToFiles() { + Logger.getAnonymousLogger().log(Level.INFO, "categorizing{0} as {1}", new Object[]{selectedFileIDs.toString(), cat.getDisplayName()}); //NON-NLS + controller.queueDBWorkerTask(new CategorizeTask(selectedFileIDs, cat, createUndo)); } /** @@ -101,49 +97,43 @@ public class CategorizeAction extends AddTagAction { CategoryMenu(ImageGalleryController controller) { super(Bundle.CategorizeAction_displayName()); + setGraphic(new ImageView(DrawableAttribute.CATEGORY.getIcon())); + ObservableSet selected = controller.getSelectionModel().getSelected(); // Each category get an item in the sub-menu. Selecting one of these menu items adds // a tag with the associated category. for (final Category cat : Category.values()) { - - MenuItem categoryItem = new MenuItem(cat.getDisplayName()); - categoryItem.setOnAction((ActionEvent t) -> { - final CategorizeAction categorizeAction = new CategorizeAction(controller); - categorizeAction.addTag(controller.getCategoryManager().getTagName(cat), NO_COMMENT); - }); - categoryItem.setAccelerator(new KeyCodeCombination(KeyCode.getKeyCode(Integer.toString(cat.getCategoryNumber())))); + MenuItem categoryItem = ActionUtils.createMenuItem(new CategorizeAction(controller, cat, selected)); getItems().add(categoryItem); } } } @NbBundle.Messages({"# {0} - fileID number", - "CategorizeTask.errorUnable.msg=Unable to categorize {0}.", - "CategorizeTask.errorUnable.title=Categorizing Error"}) - private class CategorizeTask extends ImageGalleryController.InnerTask { + "CategorizeTask.errorUnable.msg=Unable to categorize {0}.", + "CategorizeTask.errorUnable.title=Categorizing Error"}) + private class CategorizeTask extends ImageGalleryController.BackgroundTask { private final Set fileIDs; - @Nonnull - private final TagName tagName; - private final String comment; - private final boolean createUndo; - CategorizeTask(Set fileIDs, @Nonnull TagName tagName, String comment, boolean createUndo) { + private final boolean createUndo; + private final Category cat; + + CategorizeTask(Set fileIDs, @Nonnull Category cat, boolean createUndo) { super(); this.fileIDs = fileIDs; - java.util.Objects.requireNonNull(tagName); - this.tagName = tagName; - this.comment = comment; + java.util.Objects.requireNonNull(cat); + this.cat = cat; this.createUndo = createUndo; - } - @Override public void run() { final DrawableTagsManager tagsManager = controller.getTagsManager(); final CategoryManager categoryManager = controller.getCategoryManager(); - Map oldCats = new HashMap<>(); + Map oldCats = new HashMap<>(); + TagName tagName = categoryManager.getTagName(cat); + TagName catZeroTagName = categoryManager.getTagName(Category.ZERO); for (long fileID : fileIDs) { try { DrawableFile file = controller.getFileFromId(fileID); //drawable db access @@ -151,12 +141,12 @@ public class CategorizeAction extends AddTagAction { Category oldCat = file.getCategory(); //drawable db access TagName oldCatTagName = categoryManager.getTagName(oldCat); if (false == tagName.equals(oldCatTagName)) { - oldCats.put(fileID, oldCatTagName); + oldCats.put(fileID, oldCat); } } final List fileTags = tagsManager.getContentTagsByContent(file); - if (tagName == categoryManager.getTagName(Category.ZERO)) { + if (tagName.equals(catZeroTagName)) { // delete all cat tags for cat-0 fileTags.stream() .filter(tag -> CategoryManager.isCategoryTagName(tag.getName())) @@ -173,7 +163,7 @@ public class CategorizeAction extends AddTagAction { .map(Tag::getName) .filter(tagName::equals) .collect(Collectors.toList()).isEmpty()) { - tagsManager.addContentTag(file, tagName, comment); + tagsManager.addContentTag(file, tagName, ""); } } } catch (TskCoreException ex) { @@ -186,7 +176,7 @@ public class CategorizeAction extends AddTagAction { } if (createUndo && oldCats.isEmpty() == false) { - undoManager.addToUndo(new CategorizationChange(controller, tagName, oldCats)); + undoManager.addToUndo(new CategorizationChange(controller, cat, oldCats)); } } } @@ -197,11 +187,11 @@ public class CategorizeAction extends AddTagAction { @Immutable private final class CategorizationChange implements UndoRedoManager.UndoableCommand { - private final TagName newCategory; - private final ImmutableMap oldCategories; + private final Category newCategory; + private final ImmutableMap oldCategories; private final ImageGalleryController controller; - CategorizationChange(ImageGalleryController controller, TagName newCategory, Map oldCategories) { + CategorizationChange(ImageGalleryController controller, Category newCategory, Map oldCategories) { this.controller = controller; this.newCategory = newCategory; this.oldCategories = ImmutableMap.copyOf(oldCategories); @@ -213,8 +203,8 @@ public class CategorizeAction extends AddTagAction { */ @Override public void run() { - CategorizeAction categorizeAction = new CategorizeAction(controller); - categorizeAction.addTagsToFiles(newCategory, "", this.oldCategories.keySet(), false); + CategorizeAction categorizeAction = new CategorizeAction(controller, newCategory, this.oldCategories.keySet(), false); + categorizeAction.addCatToFiles(); } /** @@ -223,9 +213,10 @@ public class CategorizeAction extends AddTagAction { */ @Override public void undo() { - CategorizeAction categorizeAction = new CategorizeAction(controller); - for (Map.Entry entry : oldCategories.entrySet()) { - categorizeAction.addTagsToFiles(entry.getValue(), "", Collections.singleton(entry.getKey()), false); + + for (Map.Entry entry : oldCategories.entrySet()) { + new CategorizeAction(controller, entry.getValue(), Collections.singleton(entry.getKey()), false) + .addCatToFiles(); } } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeGroupAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeGroupAction.java index b060b645c9..fd411a4d5a 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeGroupAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeGroupAction.java @@ -19,21 +19,19 @@ package org.sleuthkit.autopsy.imagegallery.actions; import com.google.common.collect.ImmutableSet; -import java.util.Set; -import org.controlsfx.control.action.Action; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.datamodel.Category; /** * */ -public class CategorizeGroupAction extends Action { +public class CategorizeGroupAction extends CategorizeAction { public CategorizeGroupAction(Category cat, ImageGalleryController controller) { - super(cat.getDisplayName(), (javafx.event.ActionEvent actionEvent) -> { - Set fileIdSet = ImmutableSet.copyOf(controller.viewState().get().getGroup().getFileIDs()); - new CategorizeAction(controller).addTagsToFiles(controller.getTagsManager().getTagName(cat), "", fileIdSet); - }); - setGraphic(cat.getGraphic()); + super(controller, cat, null); + setEventHandler(actionEvent -> + new CategorizeAction(controller, cat, ImmutableSet.copyOf(controller.viewState().get().getGroup().getFileIDs())) + .handle(actionEvent) + ); } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeSelectedFilesAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeSelectedFilesAction.java index 04a2a93e9e..e1a5b3e94c 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeSelectedFilesAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeSelectedFilesAction.java @@ -18,17 +18,19 @@ */ package org.sleuthkit.autopsy.imagegallery.actions; -import org.controlsfx.control.action.Action; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.datamodel.Category; /** * */ -public class CategorizeSelectedFilesAction extends Action { +public class CategorizeSelectedFilesAction extends CategorizeAction { public CategorizeSelectedFilesAction(Category cat, ImageGalleryController controller) { - super(cat.getDisplayName(), (javafx.event.ActionEvent actionEvent) -> new CategorizeAction(controller).addTag(controller.getTagsManager().getTagName(cat), "")); - setGraphic(cat.getGraphic()); + super(controller, cat, null); + setEventHandler(actionEvent -> + new CategorizeAction(controller, cat, controller.getSelectionModel().getSelected()) + .handle(actionEvent) + ); } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/TagGroupAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/TagGroupAction.java index c53d796a60..afd6a7dcfc 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/TagGroupAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/TagGroupAction.java @@ -19,21 +19,19 @@ package org.sleuthkit.autopsy.imagegallery.actions; import com.google.common.collect.ImmutableSet; -import java.util.Set; -import org.controlsfx.control.action.Action; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.datamodel.TagName; /** * */ -public class TagGroupAction extends Action { +public class TagGroupAction extends AddTagAction { public TagGroupAction(final TagName tagName, ImageGalleryController controller) { - super(tagName.getDisplayName(), (javafx.event.ActionEvent actionEvent) -> { - Set fileIdSet = ImmutableSet.copyOf(controller.viewState().get().getGroup().getFileIDs()); - new AddDrawableTagAction(controller).addTagsToFiles(tagName, "", fileIdSet); - }); - setGraphic(controller.getTagsManager().getGraphic(tagName)); + super(controller, tagName, null); + setEventHandler(actionEvent -> + new AddTagAction(controller, tagName, ImmutableSet.copyOf(controller.viewState().get().getGroup().getFileIDs())). + handle(actionEvent) + ); } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/TagSelectedFilesAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/TagSelectedFilesAction.java index 3aafc804a1..b8c8474080 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/TagSelectedFilesAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/TagSelectedFilesAction.java @@ -18,17 +18,19 @@ */ package org.sleuthkit.autopsy.imagegallery.actions; -import org.controlsfx.control.action.Action; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.datamodel.TagName; /** * */ -public class TagSelectedFilesAction extends Action { +public class TagSelectedFilesAction extends AddTagAction { public TagSelectedFilesAction(final TagName tagName, ImageGalleryController controller) { - super(tagName.getDisplayName(), actionEvent -> new AddDrawableTagAction(controller).addTag(tagName, "")); - setGraphic(controller.getTagsManager().getGraphic(tagName)); + super(controller, tagName, null); + setEventHandler(actionEvent -> + new AddTagAction(controller, tagName, controller.getSelectionModel().getSelected()). + handle(actionEvent) + ); } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/Category.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/Category.java index 36d84f90e7..1b2bd604b0 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/Category.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/Category.java @@ -25,6 +25,9 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import javafx.geometry.Insets; import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import javafx.scene.layout.Background; import javafx.scene.layout.BackgroundFill; import javafx.scene.layout.Border; @@ -94,6 +97,7 @@ public enum Category { private final String displayName; private final int id; + private Image snapshot; private Category(Color color, int id, String name) { this.color = color; @@ -118,11 +122,15 @@ public enum Category { return displayName; } - public Node getGraphic() { - Region region = new Region(); - region.setBackground(new Background(new BackgroundFill(getColor(), CORNER_RADII_4, Insets.EMPTY))); - region.setPrefSize(16, 16); - region.setBorder(new Border(new BorderStroke(getColor().darker(), BorderStrokeStyle.SOLID, CORNER_RADII_4, BORDER_WIDTHS_2))); - return region; + synchronized public Node getGraphic() { + if (snapshot == null) { + Region region = new Region(); + region.setBackground(new Background(new BackgroundFill(getColor(), CORNER_RADII_4, Insets.EMPTY))); + region.setPrefSize(16, 16); + region.setBorder(new Border(new BorderStroke(getColor().darker(), BorderStrokeStyle.SOLID, CORNER_RADII_4, BORDER_WIDTHS_2))); + Scene scene = new Scene(region, 16, 16, Color.TRANSPARENT); + snapshot = region.snapshot(null, null); + } + return new ImageView(snapshot); } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java index 034fbe3b03..cb842ed1df 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java @@ -903,16 +903,11 @@ public final class DrawableDB { StringBuilder query = new StringBuilder("SELECT " + groupBy.attrName.toString() + ", COUNT(*) FROM drawable_files GROUP BY " + groupBy.attrName.toString()); //NON-NLS String orderByClause = ""; - switch (sortBy) { - case GROUP_BY_VALUE: - orderByClause = " ORDER BY " + groupBy.attrName.toString(); //NON-NLS - break; - case FILE_COUNT: - orderByClause = " ORDER BY COUNT(*)"; //NON-NLS - break; - case NONE: -// case PRIORITY: - break; + + if (sortBy == GROUP_BY_VALUE) { + orderByClause = " ORDER BY " + groupBy.attrName.toString(); + } else if (sortBy == GroupSortBy.FILE_COUNT) { + orderByClause = " ORDER BY COUNT(*)"; } query.append(orderByClause); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableTagsManager.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableTagsManager.java index ea2f17f62e..902aa286d1 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableTagsManager.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableTagsManager.java @@ -45,13 +45,24 @@ import org.sleuthkit.datamodel.TskCoreException; * Manages Tags, Tagging, and the relationship between Categories and Tags in * the autopsy Db. Delegates some work to the backing {@link TagsManager}. */ -@NbBundle.Messages({"DrawableTagsManager.followUp=Follow Up"}) +@NbBundle.Messages({"DrawableTagsManager.followUp=Follow Up", + "DrawableTagsManager.bookMark=Bookmark"}) public class DrawableTagsManager { private static final Logger LOGGER = Logger.getLogger(DrawableTagsManager.class.getName()); private static final String FOLLOW_UP = Bundle.DrawableTagsManager_followUp(); + private static final String BOOKMARK = Bundle.DrawableTagsManager_bookMark(); private static Image FOLLOW_UP_IMAGE; + private static Image BOOKMARK_IMAGE; + + public static String getFollowUpText() { + return FOLLOW_UP; + } + + public static String getBookmarkText() { + return BOOKMARK; + } final private Object autopsyTagsManagerLock = new Object(); private TagsManager autopsyTagsManager; @@ -70,6 +81,7 @@ public class DrawableTagsManager { * The tag name corresponding to the "built-in" tag "Follow Up" */ private TagName followUpTagName; + private TagName bookmarkTagName; public DrawableTagsManager(TagsManager autopsyTagsManager) { this.autopsyTagsManager = autopsyTagsManager; @@ -141,6 +153,15 @@ public class DrawableTagsManager { } } + private Object getBookmarkTagName() throws TskCoreException { + synchronized (autopsyTagsManagerLock) { + if (Objects.isNull(bookmarkTagName)) { + bookmarkTagName = getTagName(BOOKMARK); + } + return bookmarkTagName; + } + } + /** * get all the TagNames that are not categories * @@ -192,7 +213,7 @@ public class DrawableTagsManager { } catch (TagsManager.TagNameAlreadyExistsException ex) { throw new TskCoreException("tagame exists but wasn't found", ex); } - } catch (IllegalStateException ex) { + } catch (NullPointerException | IllegalStateException ex) { LOGGER.log(Level.SEVERE, "Case was closed out from underneath", ex); //NON-NLS throw new TskCoreException("Case was closed out from underneath", ex); } @@ -241,9 +262,11 @@ public class DrawableTagsManager { try { if (tagname.equals(getFollowUpTagName())) { return new ImageView(getFollowUpImage()); + } else if (tagname.equals(getBookmarkTagName())) { + return new ImageView(getBookmarkImage()); } } catch (TskCoreException ex) { - LOGGER.log(Level.SEVERE, "Failed to get \"Follow Up\" tag name from db.", ex); + LOGGER.log(Level.SEVERE, "Failed to get \"Follow Up\" or \"Bookmark\"tag name from db.", ex); } return DrawableAttribute.TAGS.getGraphicForValue(tagname); } @@ -254,4 +277,12 @@ public class DrawableTagsManager { } return FOLLOW_UP_IMAGE; } + + synchronized private static Image getBookmarkImage() { + if (BOOKMARK_IMAGE == null) { + BOOKMARK_IMAGE = new Image("/org/sleuthkit/autopsy/images/star-bookmark-icon-16.png"); + } + return BOOKMARK_IMAGE; + } + } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java index cebcd6c222..bdc659e416 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java @@ -25,6 +25,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -126,7 +127,7 @@ public class GroupManager { private volatile DrawableAttribute groupBy = DrawableAttribute.PATH; private volatile SortOrder sortOrder = SortOrder.ASCENDING; - private final ReadOnlyObjectWrapper sortByProp = new ReadOnlyObjectWrapper<>(sortBy); + private final ReadOnlyObjectWrapper< Comparator> sortByProp = new ReadOnlyObjectWrapper<>(sortBy); private final ReadOnlyObjectWrapper< DrawableAttribute> groupByProp = new ReadOnlyObjectWrapper<>(groupBy); private final ReadOnlyObjectWrapper sortOrderProp = new ReadOnlyObjectWrapper<>(sortOrder); @@ -274,7 +275,7 @@ public class GroupManager { } else if (unSeenGroups.contains(group) == false) { unSeenGroups.add(group); } - FXCollections.sort(unSeenGroups, sortBy.getGrpComparator(sortOrder)); + FXCollections.sort(unSeenGroups, applySortOrder(sortOrder, sortBy)); } /** @@ -299,11 +300,11 @@ public class GroupManager { Platform.runLater(() -> { if (analyzedGroups.contains(group)) { analyzedGroups.remove(group); - FXCollections.sort(analyzedGroups, sortBy.getGrpComparator(sortOrder)); + FXCollections.sort(analyzedGroups, applySortOrder(sortOrder, sortBy)); } if (unSeenGroups.contains(group)) { unSeenGroups.remove(group); - FXCollections.sort(unSeenGroups, sortBy.getGrpComparator(sortOrder)); + FXCollections.sort(unSeenGroups, applySortOrder(sortOrder, sortBy)); } }); } @@ -450,7 +451,7 @@ public class GroupManager { } } - public GroupSortBy getSortBy() { + public Comparator getSortBy() { return sortBy; } @@ -459,7 +460,7 @@ public class GroupManager { Platform.runLater(() -> sortByProp.set(sortBy)); } - public ReadOnlyObjectProperty getSortByProperty() { + public ReadOnlyObjectProperty< Comparator> getSortByProperty() { return sortByProp.getReadOnlyProperty(); } @@ -523,8 +524,8 @@ public class GroupManager { setSortBy(sortBy); setSortOrder(sortOrder); Platform.runLater(() -> { - FXCollections.sort(analyzedGroups, sortBy.getGrpComparator(sortOrder)); - FXCollections.sort(unSeenGroups, sortBy.getGrpComparator(sortOrder)); + FXCollections.sort(analyzedGroups, applySortOrder(sortOrder, sortBy)); + FXCollections.sort(unSeenGroups, applySortOrder(sortOrder, sortBy)); }); } } @@ -666,7 +667,7 @@ public class GroupManager { group = new DrawableGroup(groupKey, fileIDs, groupSeen); controller.getCategoryManager().registerListener(group); group.seenProperty().addListener((o, oldSeen, newSeen) -> { - markGroupSeen(group, newSeen); + Platform.runLater(() -> markGroupSeen(group, newSeen)); }); groupMap.put(groupKey, group); } @@ -675,7 +676,7 @@ public class GroupManager { if (analyzedGroups.contains(group) == false) { analyzedGroups.add(group); if (Objects.isNull(task)) { - FXCollections.sort(analyzedGroups, sortBy.getGrpComparator(sortOrder)); + FXCollections.sort(analyzedGroups, applySortOrder(sortOrder, sortBy)); } } markGroupSeen(group, groupSeen); @@ -719,12 +720,12 @@ public class GroupManager { */ @SuppressWarnings({"unchecked", "rawtypes"}) @NbBundle.Messages({"# {0} - groupBy attribute Name", - "# {1} - sortBy name", - "# {2} - sort Order", - "ReGroupTask.displayTitle=regrouping files by {0} sorted by {1} in {2} order", - "# {0} - groupBy attribute Name", - "# {1} - atribute value", - "ReGroupTask.progressUpdate=regrouping files by {0} : {1}"}) + "# {1} - sortBy name", + "# {2} - sort Order", + "ReGroupTask.displayTitle=regrouping files by {0} sorted by {1} in {2} order", + "# {0} - groupBy attribute Name", + "# {1} - atribute value", + "ReGroupTask.progressUpdate=regrouping files by {0} : {1}"}) private class ReGroupTask> extends LoggedTask { private ProgressHandle groupProgress; @@ -735,8 +736,8 @@ public class GroupManager { private final SortOrder sortOrder; - public ReGroupTask(DrawableAttribute groupBy, GroupSortBy sortBy, SortOrder sortOrder) { - super(Bundle.ReGroupTask_displayTitle(groupBy.attrName.toString(), sortBy.name(), sortOrder.toString()), true); + ReGroupTask(DrawableAttribute groupBy, GroupSortBy sortBy, SortOrder sortOrder) { + super(Bundle.ReGroupTask_displayTitle(groupBy.attrName.toString(), sortBy.getDisplayName(), sortOrder.toString()), true); this.groupBy = groupBy; this.sortBy = sortBy; @@ -755,7 +756,7 @@ public class GroupManager { return null; } - groupProgress = ProgressHandleFactory.createHandle(Bundle.ReGroupTask_displayTitle(groupBy.attrName.toString(), sortBy.name(), sortOrder.toString()), this); + groupProgress = ProgressHandleFactory.createHandle(Bundle.ReGroupTask_displayTitle(groupBy.attrName.toString(), sortBy.getDisplayName(), sortOrder.toString()), this); Platform.runLater(() -> { analyzedGroups.clear(); unSeenGroups.clear(); @@ -778,7 +779,7 @@ public class GroupManager { groupProgress.progress(Bundle.ReGroupTask_progressUpdate(groupBy.attrName.toString(), val), p); popuplateIfAnalyzed(new GroupKey(groupBy, val), this); } - Platform.runLater(() -> FXCollections.sort(analyzedGroups, sortBy.getGrpComparator(sortOrder))); + Platform.runLater(() -> FXCollections.sort(analyzedGroups, applySortOrder(sortOrder, sortBy))); updateProgress(1, 1); return null; @@ -793,4 +794,16 @@ public class GroupManager { } } } + + private static Comparator applySortOrder(final SortOrder sortOrder, Comparator comparator) { + switch (sortOrder) { + case ASCENDING: + return comparator; + case DESCENDING: + return comparator.reversed(); + case UNSORTED: + default: + return new GroupSortBy.AllEqualComparator<>(); + } + } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupSortBy.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupSortBy.java index 5d8740b343..8239837d6a 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupSortBy.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupSortBy.java @@ -18,89 +18,50 @@ */ package org.sleuthkit.autopsy.imagegallery.datamodel.grouping; -import java.util.Arrays; import java.util.Comparator; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.scene.image.Image; -import javax.swing.SortOrder; -import static javax.swing.SortOrder.ASCENDING; -import static javax.swing.SortOrder.DESCENDING; import org.apache.commons.lang3.StringUtils; import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; -import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute; /** - * enum of possible properties to sort groups by. This is the model for the drop - * down in Toolbar as well as each enum value having the stategy - * ({@link Comparator}) for sorting the groups + * Pseudo enum of possible properties to sort groups by. */ @NbBundle.Messages({"GroupSortBy.groupSize=Group Size", - "GroupSortBy.groupName=Group Name", - "GroupSortBy.none=None", - "GroupSortBy.priority=Priority"}) -public enum GroupSortBy implements ComparatorProvider { + "GroupSortBy.groupName=Group Name", + "GroupSortBy.none=None", + "GroupSortBy.priority=Priority"}) +public class GroupSortBy implements Comparator { /** - * sort the groups by the number of files in each sort the groups by the - * number of files in each + * sort the groups by the number of files in each */ - FILE_COUNT(Bundle.GroupSortBy_groupSize(), true, "folder-open-image.png") { //NON-NLS - @Override - public Comparator getGrpComparator(final SortOrder sortOrder) { - return applySortOrder(sortOrder, Comparator.comparingInt(DrawableGroup::getSize)); - } + public final static GroupSortBy FILE_COUNT = new GroupSortBy(Bundle.GroupSortBy_groupSize(), "folder-open-image.png", Comparator.comparing(DrawableGroup::getSize)); - @Override - public > Comparator getValueComparator(final DrawableAttribute attr, final SortOrder sortOrder) { - return getDefaultValueComparator(attr, sortOrder); - } - }, /** * sort the groups by the natural order of the grouping value ( eg group * them by path alphabetically ) */ - GROUP_BY_VALUE(Bundle.GroupSortBy_groupName(), true, "folder-rename.png") { //NON-NLS - @Override - public Comparator getGrpComparator(final SortOrder sortOrder) { - return applySortOrder(sortOrder, Comparator.comparing(t -> t.getGroupByValueDislpayName())); - } + public final static GroupSortBy GROUP_BY_VALUE = new GroupSortBy(Bundle.GroupSortBy_groupName(), "folder-rename.png", Comparator.comparing(DrawableGroup::getGroupByValueDislpayName)); - @Override - public > Comparator getValueComparator(final DrawableAttribute attr, final SortOrder sortOrder) { - return applySortOrder(sortOrder, Comparator.naturalOrder()); - } - }, /** * don't sort the groups just use what ever order they come in (ingest * order) */ - NONE(Bundle.GroupSortBy_none(), false, "prohibition.png") { //NON-NLS - @Override - public Comparator getGrpComparator(SortOrder sortOrder) { - return new NoOpComparator<>(); - } + public final static GroupSortBy NONE = new GroupSortBy(Bundle.GroupSortBy_none(), "prohibition.png", new AllEqualComparator<>()); - @Override - public > Comparator getValueComparator(DrawableAttribute attr, final SortOrder sortOrder) { - return new NoOpComparator<>(); - } - }, /** * sort the groups by some priority metric to be determined and implemented */ - PRIORITY(Bundle.GroupSortBy_priority(), false, "hashset_hits.png") { //NON-NLS - @Override - public Comparator getGrpComparator(SortOrder sortOrder) { - return Comparator.nullsLast(Comparator.comparingDouble(DrawableGroup::getHashHitDensity).thenComparingInt(DrawableGroup::getSize).reversed()); - } + public final static GroupSortBy PRIORITY = new GroupSortBy(Bundle.GroupSortBy_priority(), "hashset_hits.png", Comparator.comparing(DrawableGroup::getHashHitDensity).thenComparing(Comparator.comparing(DrawableGroup::getUncategorizedCount))); - @Override - public > Comparator getValueComparator(DrawableAttribute attr, SortOrder sortOrder) { - return getDefaultValueComparator(attr, sortOrder); - } - }; + @Override + public int compare(DrawableGroup o1, DrawableGroup o2) { + return delegate.compare(o1, o2); + } + + private final static ObservableList values = FXCollections.unmodifiableObservableList(FXCollections.observableArrayList(PRIORITY, NONE, GROUP_BY_VALUE, FILE_COUNT)); /** * get a list of the values of this enum @@ -108,8 +69,7 @@ public enum GroupSortBy implements ComparatorProvider { * @return */ public static ObservableList getValues() { - return FXCollections.observableArrayList(Arrays.asList(values())); - + return values; } final private String displayName; @@ -118,12 +78,12 @@ public enum GroupSortBy implements ComparatorProvider { private final String imageName; - private final Boolean sortOrderEnabled; + private final Comparator delegate; - private GroupSortBy(String displayName, Boolean sortOrderEnabled, String imagePath) { + private GroupSortBy(String displayName, String imagePath, Comparator internalComparator) { this.displayName = displayName; - this.sortOrderEnabled = sortOrderEnabled; this.imageName = imagePath; + this.delegate = internalComparator; } public String getDisplayName() { @@ -139,49 +99,11 @@ public enum GroupSortBy implements ComparatorProvider { return icon; } - public Boolean isSortOrderEnabled() { - return sortOrderEnabled; - } - - private static Comparator applySortOrder(final SortOrder sortOrder, Comparator comparator) { - switch (sortOrder) { - case ASCENDING: - return comparator; - case DESCENDING: - return comparator.reversed(); - case UNSORTED: - default: - return new NoOpComparator<>(); - } - } - - private static class NoOpComparator implements Comparator { + static class AllEqualComparator implements Comparator { @Override public int compare(A o1, A o2) { return 0; } } - -} - -/** - * * implementers of this interface must provide a method to compare - * ({@link Comparable}) values and Groupings based on an - * {@link DrawableAttribute} and a {@link SortOrder} - */ -interface ComparatorProvider { - - > Comparator getValueComparator(DrawableAttribute attr, SortOrder sortOrder); - - Comparator getGrpComparator(SortOrder sortOrder); - - default > Comparator getDefaultValueComparator(DrawableAttribute attr, SortOrder sortOrder) { - return (A v1, A v2) -> { - DrawableGroup g1 = ImageGalleryController.getDefault().getGroupManager().getGroupForKey(new GroupKey<>(attr, v1)); - DrawableGroup g2 = ImageGalleryController.getDefault().getGroupManager().getGroupForKey(new GroupKey<>(attr, v2)); - - return getGrpComparator(sortOrder).compare(g1, g2); - }; - } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SortChooser.fxml b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SortChooser.fxml new file mode 100644 index 0000000000..432b47d8c7 --- /dev/null +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SortChooser.fxml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SortChooser.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SortChooser.java new file mode 100644 index 0000000000..49543d5f2d --- /dev/null +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SortChooser.java @@ -0,0 +1,181 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2016 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.gui; + +import java.lang.reflect.InvocationTargetException; +import java.util.Comparator; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ComboBox; +import javafx.scene.control.ListCell; +import javafx.scene.control.RadioButton; +import javafx.scene.control.ToggleGroup; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; +import javax.swing.SortOrder; +import org.sleuthkit.autopsy.imagegallery.FXMLConstructor; + +/** + * + */ +public class SortChooser> extends HBox { + + @FXML + private RadioButton ascRadio; + @FXML + private RadioButton descRadio; + @FXML + private ToggleGroup orderGroup; + @FXML + private ComboBox sortByBox; + + private final ObservableList comparators; + + private final ReadOnlyObjectWrapper sortOrder = new ReadOnlyObjectWrapper<>(SortOrder.ASCENDING); + private final SimpleBooleanProperty sortOrderDisabled = new SimpleBooleanProperty(false); + private final SimpleObjectProperty valueType = new SimpleObjectProperty<>(ValueType.NUMERIC); + + public SortChooser(ObservableList comps) { + this.comparators = comps; + FXMLConstructor.construct(this, "SortChooser.fxml"); + } + + @FXML + void initialize() { + assert ascRadio != null : "fx:id=\"ascRadio\" was not injected: check your FXML file 'Toolbar.fxml'."; + assert descRadio != null : "fx:id=\"descRadio\" was not injected: check your FXML file 'Toolbar.fxml'."; + assert orderGroup != null : "fx:id=\"orderGroup\" was not injected: check your FXML file 'Toolbar.fxml'."; + assert sortByBox != null : "fx:id=\"sortByBox\" was not injected: check your FXML file 'Toolbar.fxml'."; + + ascRadio.getStyleClass().remove("radio-button"); + ascRadio.getStyleClass().add("toggle-button"); + descRadio.getStyleClass().remove("radio-button"); + descRadio.getStyleClass().add("toggle-button"); + + valueType.addListener((observable, oldValue, newValue) -> { + ascRadio.setGraphic(new ImageView(newValue.getAscendingImage())); + descRadio.setGraphic(new ImageView(newValue.getDescendingImage())); + }); + + ascRadio.disableProperty().bind(sortOrderDisabled); + descRadio.disableProperty().bind(sortOrderDisabled); + ascRadio.selectedProperty().addListener(selectedToggle -> { + sortOrder.set(orderGroup.getSelectedToggle() == ascRadio ? SortOrder.ASCENDING : SortOrder.DESCENDING); + }); + + sortByBox.setItems(comparators); + sortByBox.setCellFactory(listView -> new ComparatorCell()); + sortByBox.setButtonCell(new ComparatorCell()); + } + + public ValueType getValueType() { + return valueType.get(); + } + + public void setValueType(ValueType type) { + valueType.set(type); + } + + public SimpleObjectProperty valueTypeProperty() { + return valueType; + } + + public void setSortOrderDisabled(boolean disabled) { + sortOrderDisabled.set(disabled); + } + + public boolean isSortOrderDisabled() { + return sortOrderDisabled.get(); + } + + public SimpleBooleanProperty sortOrderDisabledProperty() { + return sortOrderDisabled; + } + + public SortOrder getSortOrder() { + return sortOrder.get(); + } + + public ReadOnlyObjectProperty sortOrderProperty() { + return sortOrder.getReadOnlyProperty(); + } + + public Y getComparator() { + return sortByBox.getSelectionModel().getSelectedItem(); + } + + public void setComparator(Y selected) { + sortByBox.getSelectionModel().select(selected); + } + + public ReadOnlyObjectProperty comparatorProperty() { + return sortByBox.getSelectionModel().selectedItemProperty(); + } + + public enum ValueType { + + LEXICOGRAPHIC("sort_asc_az.png", "sort_desc_az.png"), + NUMERIC("sort_ascending.png", "sort_descending.png"); + + private final Image ascImage; + private final Image descImage; + + private ValueType(String ascImageName, String descImageName) { + this.ascImage = new Image("/org/sleuthkit/autopsy/imagegallery/images/" + ascImageName); + this.descImage = new Image("/org/sleuthkit/autopsy/imagegallery/images/" + descImageName); + } + + private Image getAscendingImage() { + return ascImage; + } + + private Image getDescendingImage() { + return descImage; + } + } + + private class ComparatorCell extends ListCell { + + @Override + protected void updateItem(Y item, boolean empty) { + super.updateItem(item, empty); //To change body of generated methods, choose Tools | Templates. + + if (empty || null == item) { + setText(null); + setGraphic(null); + } else { + try { + String displayName = (String) item.getClass().getMethod("getDisplayName").invoke(item); + setText(displayName); + Image icon = (Image) item.getClass().getMethod("getIcon").invoke(item); + setGraphic(new ImageView(icon)); + } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { +// Exceptions.printStackTrace(ex); + setText(item.toString()); + setGraphic(null); + } + } + } + } +} diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/StatusBar.fxml b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/StatusBar.fxml index 03d26395ce..b02e2e2496 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/StatusBar.fxml +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/StatusBar.fxml @@ -1,16 +1,20 @@ - - - - - + + + + + + + + + - + - + @@ -18,7 +22,10 @@ + + + + @@ -27,10 +34,13 @@ - @@ -42,19 +52,6 @@ -
- - - - - -