diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java index 8e60aefbf1..b93f362dae 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java @@ -20,7 +20,6 @@ package org.sleuthkit.autopsy.imagegallery; import java.beans.PropertyChangeEvent; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; @@ -58,8 +57,10 @@ import org.sleuthkit.autopsy.coreutils.History; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.imagegallery.datamodel.Category; +import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableDB; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile; +import org.sleuthkit.autopsy.imagegallery.datamodel.HashSetManager; import org.sleuthkit.autopsy.imagegallery.grouping.GroupManager; import org.sleuthkit.autopsy.imagegallery.grouping.GroupViewState; import org.sleuthkit.autopsy.imagegallery.gui.NoGroupsDialog; @@ -124,6 +125,8 @@ public final class ImageGalleryController { private DrawableDB db; private final GroupManager groupManager = new GroupManager(this); + private final HashSetManager hashSetManager = new HashSetManager(); + private final CategoryManager categoryManager = new CategoryManager(); private StackPane fullUIStackPane; @@ -341,7 +344,7 @@ public final class ImageGalleryController { * @param theNewCase the case to configure the controller for */ public synchronized void setCase(Case theNewCase) { - this.db = DrawableDB.getDrawableDB(ImageGalleryModule.getModuleOutputDir(theNewCase), this); + this.db = DrawableDB.getDrawableDB(ImageGalleryModule.getModuleOutputDir(theNewCase), getSleuthKitCase()); setListeningEnabled(ImageGalleryModule.isEnabledforCase(theNewCase)); setStale(ImageGalleryModule.isDrawableDBStale(theNewCase)); @@ -351,8 +354,9 @@ public final class ImageGalleryController { restartWorker(); historyManager.clear(); groupManager.setDB(db); - db.initializeImageList(); - SummaryTablePane.getDefault().handleCategoryChanged(Collections.emptyList()); + hashSetManager.setDb(db); + categoryManager.setDb(db); + SummaryTablePane.getDefault().refresh(); } /** @@ -381,7 +385,7 @@ public final class ImageGalleryController { * * @param innerTask */ - public final void queueDBWorkerTask(InnerTask innerTask) { + public void queueDBWorkerTask(InnerTask innerTask) { // @@@ We could make a lock for the worker thread if (dbWorkerThread == null) { @@ -400,7 +404,7 @@ public final class ImageGalleryController { Platform.runLater(this::checkForGroups); } - public final ReadOnlyIntegerProperty getFileUpdateQueueSizeProperty() { + public ReadOnlyIntegerProperty getFileUpdateQueueSizeProperty() { return queueSizeProperty.getReadOnlyProperty(); } @@ -476,6 +480,14 @@ public final class ImageGalleryController { }); } + public HashSetManager getHashSetManager() { + return hashSetManager; + } + + public CategoryManager getCategoryManager() { + return categoryManager; + } + // @@@ REVIEW IF THIS SHOLD BE STATIC... //TODO: concept seems like the controller deal with how much work to do at a given time // @@@ review this class for synchronization issues (i.e. reset and cancel being called, add, etc.) @@ -640,7 +652,7 @@ public final class ImageGalleryController { try { DrawableFile drawableFile = DrawableFile.create(getFile(), true, db.isVideoFile(getFile())); db.updateFile(drawableFile); - } catch (NullPointerException | TskCoreException ex) { + } catch (NullPointerException ex) { // This is one of the places where we get many errors if the case is closed during processing. // We don't want to print out a ton of exceptions if this is the case. if (Case.isCaseOpen()) { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryModule.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryModule.java index 595e501e59..8518d093a9 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryModule.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryModule.java @@ -162,6 +162,8 @@ public class ImageGalleryModule { * * @return true if the given file has a supported video mime type or * extension, else false + * + * //TODO: convert this to use the new FileTypeDetector? */ public static boolean isVideoFile(AbstractFile file) { try { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeAction.java index 3f66eda4a5..79a04f535d 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeAction.java @@ -26,6 +26,7 @@ import java.util.logging.Level; import javafx.event.ActionEvent; import javafx.scene.control.Menu; import javafx.scene.control.MenuItem; +import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javax.swing.JOptionPane; import org.sleuthkit.autopsy.casemodule.Case; @@ -101,7 +102,7 @@ public class CategorizeAction extends AddTagAction { final CategorizeAction categorizeAction = new CategorizeAction(); categorizeAction.addTag(cat.getTagName(), NO_COMMENT); }); - categoryItem.setAccelerator(new KeyCodeCombination(cat.getHotKeycode())); + categoryItem.setAccelerator(new KeyCodeCombination(KeyCode.getKeyCode(Integer.toString(cat.getCategoryNumber())))); getItems().add(categoryItem); } } @@ -135,11 +136,11 @@ public class CategorizeAction extends AddTagAction { //TODO: abandon using tags for categories and instead add a new column to DrawableDB if (ct.getName().getDisplayName().startsWith(Category.CATEGORY_PREFIX)) { Case.getCurrentCase().getServices().getTagsManager().deleteContentTag(ct); //tsk db - controller.getDatabase().decrementCategoryCount(Category.fromDisplayName(ct.getName().getDisplayName())); //memory/drawable db + controller.getCategoryManager().decrementCategoryCount(Category.fromDisplayName(ct.getName().getDisplayName())); //memory/drawable db } } - controller.getDatabase().incrementCategoryCount(Category.fromDisplayName(tagName.getDisplayName())); //memory/drawable db + controller.getCategoryManager().incrementCategoryCount(Category.fromDisplayName(tagName.getDisplayName())); //memory/drawable db if (tagName != Category.ZERO.getTagName()) { // no tags for cat-0 Case.getCurrentCase().getServices().getTagsManager().addContentTag(file, tagName, comment); //tsk db } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/Category.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/Category.java index 030dc5329d..780e851876 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/Category.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/Category.java @@ -18,30 +18,19 @@ */ package org.sleuthkit.autopsy.imagegallery.datamodel; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; import java.util.Map; -import java.util.Set; +import java.util.function.Function; import java.util.logging.Level; -import javafx.event.ActionEvent; -import javafx.event.EventHandler; -import javafx.scene.control.MenuItem; -import javafx.scene.control.SplitMenuButton; -import javafx.scene.image.ImageView; -import javafx.scene.input.KeyCode; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javafx.scene.paint.Color; -import javax.annotation.concurrent.GuardedBy; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.imagegallery.TagUtils; -import org.sleuthkit.autopsy.imagegallery.actions.CategorizeAction; import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.TskCoreException; /** - * + * Enum to represent the six categories in the DHs image categorization scheme. */ public enum Category implements Comparable { @@ -52,59 +41,47 @@ public enum Category implements Comparable { FOUR(Color.BISQUE, 4, "CAT-4, Exemplar/Comparison (Internal Use Only)"), FIVE(Color.GREEN, 5, "CAT-5, Non-pertinent"); - final static private Map nameMap = new HashMap<>(); - - private static final List valuesList = Arrays.asList(values()); - - static { - for (Category cat : values()) { - nameMap.put(cat.displayName, cat); - } - } - - @GuardedBy("listeners") - private final static Set listeners = new HashSet<>(); - - public static void fireChange(Collection ids) { - Set listenersCopy = new HashSet<>(); - synchronized (listeners) { - listenersCopy.addAll(listeners); - } - for (CategoryListener list : listenersCopy) { - list.handleCategoryChanged(ids); - } - - } - - public static void registerListener(CategoryListener aThis) { - synchronized (listeners) { - listeners.add(aThis); - } - } - - public static void unregisterListener(CategoryListener aThis) { - synchronized (listeners) { - listeners.remove(aThis); - } - } - - public KeyCode getHotKeycode() { - return KeyCode.getKeyCode(Integer.toString(id)); - } + /** map from displayName to enum value */ + private static final Map nameMap + = Stream.of(values()).collect(Collectors.toMap(Category::getDisplayName, + Function.identity())); public static final String CATEGORY_PREFIX = "CAT-"; - private TagName tagName; - - public static List valuesList() { - return valuesList; + public static Category fromDisplayName(String displayName) { + return nameMap.get(displayName); } - private Color color; + /** + * Use when closing a case to make sure everything is re-initialized in the + * next case. + */ + public static void clearTagNames() { + Category.ZERO.tagName = null; + Category.ONE.tagName = null; + Category.TWO.tagName = null; + Category.THREE.tagName = null; + Category.FOUR.tagName = null; + Category.FIVE.tagName = null; + } - private String displayName; + private TagName tagName; - private int id; + private final Color color; + + private final String displayName; + + private final int id; + + private Category(Color color, int id, String name) { + this.color = color; + this.displayName = name; + this.id = id; + } + + public int getCategoryNumber() { + return id; + } public Color getColor() { return color; @@ -119,18 +96,12 @@ public enum Category implements Comparable { return displayName; } - static public Category fromDisplayName(String displayName) { - return nameMap.get(displayName); - } - - private Category(Color color, int id, String name) { - this.color = color; - this.displayName = name; - this.id = id; - } - + /** + * get the TagName used to store this Category in the main autopsy db. + * + * @return the TagName used for this Category + */ public TagName getTagName() { - if (tagName == null) { try { tagName = TagUtils.getTagName(displayName); @@ -140,35 +111,4 @@ public enum Category implements Comparable { } return tagName; } - - public MenuItem createSelCatMenuItem(final SplitMenuButton catSelectedMenuButton) { - final MenuItem menuItem = new MenuItem(this.getDisplayName(), new ImageView(DrawableAttribute.CATEGORY.getIcon())); - menuItem.setOnAction(new EventHandler() { - @Override - public void handle(ActionEvent t) { - new CategorizeAction().addTag(Category.this.getTagName(), ""); - catSelectedMenuButton.setText(Category.this.getDisplayName()); - catSelectedMenuButton.setOnAction(this); - } - }); - return menuItem; - } - - /** - * Use when closing a case to make sure everything is re-initialized in the next case. - */ - public static void clearTagNames(){ - Category.ZERO.tagName = null; - Category.ONE.tagName = null; - Category.TWO.tagName = null; - Category.THREE.tagName = null; - Category.FOUR.tagName = null; - Category.FIVE.tagName = null; - } - - public static interface CategoryListener { - - public void handleCategoryChanged(Collection ids); - - } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/CategoryChangeEvent.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/CategoryChangeEvent.java new file mode 100644 index 0000000000..f53acac5f5 --- /dev/null +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/CategoryChangeEvent.java @@ -0,0 +1,44 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.datamodel; + +import java.util.Collection; +import java.util.Collections; +import javax.annotation.concurrent.Immutable; + +/** + * Event broadcast to various UI componenets when one or more files' category + * has been changed + */ +@Immutable +public class CategoryChangeEvent { + + private final Collection ids; + + /** + * @return the fileIDs of the files whose categories have changed + */ + public Collection getIds() { + return Collections.unmodifiableCollection(ids); + } + + public CategoryChangeEvent(Collection ids) { + this.ids = ids; + } +} diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/CategoryManager.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/CategoryManager.java new file mode 100644 index 0000000000..699af41bbb --- /dev/null +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/CategoryManager.java @@ -0,0 +1,172 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.datamodel; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; +import java.util.Collection; +import java.util.concurrent.atomic.LongAdder; +import java.util.logging.Level; +import org.sleuthkit.autopsy.coreutils.Logger; + +/** + * Provides a cached view of the number of files per category, and fires + * {@link CategoryChangeEvent}s when files are categorized. + * + * To receive CategoryChangeEvents, a listener must register itself, and + * implement a method annotated with {@link Subscribe} that accepts one argument + * of type CategoryChangeEvent + * + * TODO: currently these two functions (cached counts and events) are separate + * although they are related. Can they be integrated more? + * + */ +public class CategoryManager { + + private static final java.util.logging.Logger LOGGER = Logger.getLogger(CategoryManager.class.getName()); + + /** + * the DrawableDB that backs the category counts cache. The counts are + * initialized from this, and the counting of CAT-0 is always delegated to + * this db. + */ + private DrawableDB db; + + /** + * Used to distribute {@link CategoryChangeEvent}s + */ + private final EventBus categoryEventBus = new EventBus("Category Event Bus"); + + /** + * For performance reasons, keep current category counts in memory. All of + * the count related methods go through this cache, which loads initial + * values from the database if needed. + */ + private final LoadingCache categoryCounts + = CacheBuilder.newBuilder().build(CacheLoader.from(this::getCategoryCountHelper)); + + /** + * assign a new db. the counts cache is invalidated and all subsequent db + * lookups go to the new db. + * + * Also clears the Category TagNames (should this happen here?) + * + * @param db + */ + public void setDb(DrawableDB db) { + this.db = db; + categoryCounts.invalidateAll(); + Category.clearTagNames(); + } + + /** + * get the number of file with the given {@link Category} + * + * @param cat get the number of files with Category = cat + * + * @return the long the number of files with the given Category + */ + public long getCategoryCount(Category cat) { + if (cat == Category.ZERO) { + // Keeping track of the uncategorized files is a bit tricky while ingest + // is going on, so always use the list of file IDs we already have along with the + // other category counts instead of trying to track it separately. + long allOtherCatCount = getCategoryCount(Category.ONE) + getCategoryCount(Category.TWO) + getCategoryCount(Category.THREE) + getCategoryCount(Category.FOUR) + getCategoryCount(Category.FIVE); + return db.getNumberOfImageFilesInList() - allOtherCatCount; + } else { + return categoryCounts.getUnchecked(cat).sum(); + } + } + + /** + * increment the cached value for the number of files with the given + * {@link Category} + * + * @param cat the Category to increment + */ + public void incrementCategoryCount(Category cat) { + if (cat != Category.ZERO) { + categoryCounts.getUnchecked(cat).increment(); + } + } + + /** + * decrement the cached value for the number of files with the given + * {@link Category} + * + * @param cat the Category to decrement + */ + public void decrementCategoryCount(Category cat) { + if (cat != Category.ZERO) { + categoryCounts.getUnchecked(cat).decrement(); + } + } + + /** + * helper method that looks up the number of files with the given Category + * from the db and wraps it in a long adder to use in the cache + * + * + * @param cat the Category to count + * + * @return a LongAdder whose value is set to the number of file with the + * given Category + */ + private LongAdder getCategoryCountHelper(Category cat) { + LongAdder longAdder = new LongAdder(); + longAdder.decrement(); + try { + longAdder.add(db.getCategoryCount(cat)); + longAdder.increment(); + } catch (IllegalStateException ex) { + LOGGER.log(Level.WARNING, "Case closed while getting files"); + } + return longAdder; + } + + /** + * fire a CategoryChangeEvent with the given fileIDs + * + * @param fileIDs + */ + public void fireChange(Collection fileIDs) { + categoryEventBus.post(new CategoryChangeEvent(fileIDs)); + } + + /** + * register an object to receive CategoryChangeEvents + * + * @param listner + */ + public void registerListener(Object listner) { + categoryEventBus.register(listner); + } + + /** + * unregister an object from receiving CategoryChangeEvents + * + * @param listener + */ + public void unregisterListener(Object listener) { + categoryEventBus.unregister(listener); + } + +} diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java index e75d5fa0c4..4c2ce2c691 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-14 Basis Technology Corp. + * Copyright 2013-15 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,9 +35,11 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Level; +import javax.annotation.Nonnull; import javax.annotation.concurrent.GuardedBy; import javax.swing.SortOrder; import org.apache.commons.lang3.StringUtils; @@ -54,6 +56,7 @@ import static org.sleuthkit.autopsy.imagegallery.grouping.GroupSortBy.GROUP_BY_V import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TagName; @@ -65,10 +68,10 @@ import org.sqlite.SQLiteJDBCLoader; * database. This class borrows a lot of ideas and techniques (for good or ill) * from {@link SleuthkitCase}. * - * TODO: Creating an abstract base class for sqlite databases* may make sense in - * the future. see also {@link EventsDB} + * TODO: Creating an abstract base class for sqlite databases may make sense in + * the future. see also {@link EventsDB} in the timeline viewer. */ -public class DrawableDB { +public final class DrawableDB { private static final java.util.logging.Logger LOGGER = Logger.getLogger(DrawableDB.class.getName()); @@ -127,9 +130,7 @@ public class DrawableDB { */ private final HashSet updateListeners = new HashSet<>(); - private GroupManager manager; - - private ImageGalleryController controller; + private GroupManager groupManager; private final Path dbPath; @@ -146,6 +147,7 @@ public class DrawableDB { LOGGER.log(Level.SEVERE, "Failed to load sqlite JDBC driver", ex); } } + private final SleuthkitCase tskCase; //////////////general database logic , mostly borrowed from sleuthkitcase /** @@ -194,11 +196,11 @@ public class DrawableDB { * * @throws SQLException if there is problem creating or configuring the db */ - private DrawableDB(Path dbPath) throws SQLException, ExceptionInInitializerError, IOException { - + private DrawableDB(Path dbPath, SleuthkitCase tskCase) throws SQLException, ExceptionInInitializerError, IOException { this.dbPath = dbPath; + this.tskCase = tskCase; Files.createDirectories(dbPath.getParent()); - if (initializeDB()) { + if (initializeDBSchema()) { updateFileStmt = prepareStatement( "INSERT OR REPLACE INTO drawable_files (obj_id , path, name, created_time, modified_time, make, model, analyzed) " + "VALUES (?,?,?,?,?,?,?,?)"); @@ -228,9 +230,11 @@ public class DrawableDB { insertHashHitStmt = prepareStatement("insert or ignore into hash_set_hits (hash_set_id, obj_id) values (?,?)"); + initializeImageList(); } else { throw new ExceptionInInitializerError(); } + } /** @@ -280,14 +284,10 @@ public class DrawableDB { * * @return */ - public static DrawableDB getDrawableDB(Path dbPath, ImageGalleryController controller) { + public static DrawableDB getDrawableDB(Path dbPath, SleuthkitCase tskCase) { try { - Path dbFilePath = dbPath.resolve("drawable.db"); - - DrawableDB drawableDB = new DrawableDB(dbFilePath); - drawableDB.controller = controller; - return drawableDB; + return new DrawableDB(dbPath.resolve("drawable.db"), tskCase); } catch (SQLException ex) { LOGGER.log(Level.SEVERE, "sql error creating database connection", ex); return null; @@ -339,7 +339,7 @@ public class DrawableDB { * @return the number of rows in the table , count > 0 indicating an * existing table */ - private boolean initializeDB() { + private boolean initializeDBSchema() { try { if (isClosed()) { openDBCon(); @@ -954,7 +954,7 @@ public class DrawableDB { */ private DrawableFile getFileFromID(Long id, boolean analyzed) throws TskCoreException { try { - AbstractFile f = controller.getSleuthKitCase().getAbstractFileById(id); + AbstractFile f = tskCase.getAbstractFileById(id); return DrawableFile.create(f, analyzed, isVideoFile(f)); } catch (IllegalStateException ex) { LOGGER.log(Level.SEVERE, "there is no case open; failed to load file with id: " + id, ex); @@ -972,7 +972,7 @@ public class DrawableDB { */ public DrawableFile getFileFromID(Long id) throws TskCoreException { try { - AbstractFile f = controller.getSleuthKitCase().getAbstractFileById(id); + AbstractFile f = tskCase.getAbstractFileById(id); return DrawableFile.create(f, areFilesAnalyzed(Collections.singleton(id)), isVideoFile(f)); } catch (IllegalStateException ex) { @@ -986,9 +986,9 @@ public class DrawableDB { if (groupKey.getAttribute().isDBColumn) { switch (groupKey.getAttribute().attrName) { case CATEGORY: - return manager.getFileIDsWithCategory((Category) groupKey.getValue()); + return groupManager.getFileIDsWithCategory((Category) groupKey.getValue()); case TAGS: - return manager.getFileIDsWithTag((TagName) groupKey.getValue()); + return groupManager.getFileIDsWithTag((TagName) groupKey.getValue()); } } List files = new ArrayList<>(); @@ -1129,45 +1129,24 @@ public class DrawableDB { } } - /* - * The following groups of functions are used to store information in memory - * instead of in the database. Due to the change listeners in the GUI, - * this data is requested many, many times when browsing the images, and - * especially when making any changes to things like categories. + /** + * For the given fileID, get the names of all the hashsets that the file is + * in. * - * I don't like having multiple copies of the data, but these were causing - * major bottlenecks when they were all database lookups. + * @param fileID the fileID to file all the hash sets for * - * TODO: factor these out to seperate classes such as HashSetHitCache or - * CategoryCountCache + * @return a set of names, each of which is a hashset that the given file is + * in. * - * TODO: use guava Caches for this instead of lower level HashMaps + * + * //TODO: why does this go to the SKC? don't we already have this in =fo + * in the drawable db? */ - @GuardedBy("hashSetMap") - private final Map> hashSetMap = new HashMap<>(); - - @GuardedBy("hashSetMap") - public boolean isInHashSet(Long id) { - if (!hashSetMap.containsKey(id)) { - updateHashSetsForFile(id); - } - return (!hashSetMap.get(id).isEmpty()); - } - - @GuardedBy("hashSetMap") - public Set getHashSetsForFile(Long id) { - if (!isInHashSet(id)) { - updateHashSetsForFile(id); - } - return hashSetMap.get(id); - } - - @GuardedBy("hashSetMap") - public void updateHashSetsForFile(Long id) { - + @Nonnull + Set getHashSetsForFile(long fileID) { try { - List arts = ImageGalleryController.getDefault().getSleuthKitCase().getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT, id); Set hashNames = new HashSet<>(); + List arts = ImageGalleryController.getDefault().getSleuthKitCase().getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT, fileID); for (BlackboardArtifact a : arts) { List attrs = a.getAttributes(); for (BlackboardAttribute attr : attrs) { @@ -1175,48 +1154,48 @@ public class DrawableDB { hashNames.add(attr.getValueString()); } } + return hashNames; } - hashSetMap.put(id, hashNames); - } catch (IllegalStateException | TskCoreException ex) { - LOGGER.log(Level.WARNING, "could not access case during updateHashSetsForFile()", ex); - + } catch (TskCoreException tskCoreException) { + throw new IllegalStateException(tskCoreException); } + return Collections.emptySet(); } /** * For performance reasons, keep a list of all file IDs currently in the - * drawable database. - * Otherwise the database is queried many times to retrieve the same data. + * drawable database. Otherwise the database is queried many times to + * retrieve the same data. */ @GuardedBy("fileIDlist") - private final Set fileIDlist = new HashSet<>(); + private final Set fileIDsInDB = new HashSet<>(); - public boolean isDrawableFile(Long id) { - synchronized (fileIDlist) { - return fileIDlist.contains(id); + public boolean isInDB(Long id) { + synchronized (fileIDsInDB) { + return fileIDsInDB.contains(id); } } - public void addImageFileToList(Long id) { - synchronized (fileIDlist) { - fileIDlist.add(id); + private void addImageFileToList(Long id) { + synchronized (fileIDsInDB) { + fileIDsInDB.add(id); } } - public void removeImageFileFromList(Long id) { - synchronized (fileIDlist) { - fileIDlist.remove(id); + private void removeImageFileFromList(Long id) { + synchronized (fileIDsInDB) { + fileIDsInDB.remove(id); } } public int getNumberOfImageFilesInList() { - synchronized (fileIDlist) { - return fileIDlist.size(); + synchronized (fileIDsInDB) { + return fileIDsInDB.size(); } } - public void initializeImageList() { - synchronized (fileIDlist) { + private void initializeImageList() { + synchronized (fileIDsInDB) { dbReadLock(); try { Statement stmt = con.createStatement(); @@ -1224,7 +1203,6 @@ public class DrawableDB { while (analyzedQuery.next()) { addImageFileToList(analyzedQuery.getLong(OBJ_ID)); } - } catch (SQLException ex) { LOGGER.log(Level.WARNING, "problem loading file IDs: ", ex); } finally { @@ -1234,79 +1212,43 @@ public class DrawableDB { } /** - * For performance reasons, keep current category counts in memory + * For performance reasons, keep the file type in memory */ - @GuardedBy("categoryCounts") - private final Map categoryCounts = new HashMap<>(); + private final Map videoFileMap = new ConcurrentHashMap<>(); - public void incrementCategoryCount(Category cat) throws TskCoreException { - if (cat != Category.ZERO) { - synchronized (categoryCounts) { - int count = getCategoryCount(cat); - count++; - categoryCounts.put(cat, count); - } - } - } - - public void decrementCategoryCount(Category cat) throws TskCoreException { - if (cat != Category.ZERO) { - synchronized (categoryCounts) { - int count = getCategoryCount(cat); - count--; - categoryCounts.put(cat, count); - } - } - } - - public int getCategoryCount(Category cat) throws TskCoreException { - synchronized (categoryCounts) { - if (cat == Category.ZERO) { - // Keeping track of the uncategorized files is a bit tricky while ingest - // is going on, so always use the list of file IDs we already have along with the - // other category counts instead of trying to track it separately. - int allOtherCatCount = getCategoryCount(Category.ONE) + getCategoryCount(Category.TWO) + getCategoryCount(Category.THREE) - + getCategoryCount(Category.FOUR) + getCategoryCount(Category.FIVE); - return getNumberOfImageFilesInList() - allOtherCatCount; - } else if (categoryCounts.containsKey(cat)) { - return categoryCounts.get(cat); - } else { - try { - int fileCount = 0; - List contentTags = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByTagName(cat.getTagName()); - for (ContentTag ct : contentTags) { - if (ct.getContent() instanceof AbstractFile) { - AbstractFile f = (AbstractFile) ct.getContent(); - if (this.isDrawableFile(f.getId())) { - fileCount++; - } - } - } - categoryCounts.put(cat, fileCount); - return fileCount; - } catch (IllegalStateException ex) { - throw new TskCoreException("Case closed while getting files"); - } - } - } + public boolean isVideoFile(AbstractFile f) { + return videoFileMap.computeIfAbsent(f, ImageGalleryModule::isVideoFile); } /** - * For performance reasons, keep the file type in memory + * get the number of files with the given category. + * + * NOTE: although the category data is stored in autopsy as Tags, this + * method is provided on DrawableDb to provide a single point of access for + * ImageGallery data. + * + * //TODO: think about moving this and similar methods that don't actually + * get their data form the drawabledb to a layer wrapping the drawable db: + * something like ImageGalleryCaseData? + * + * @param cat the category to count the number of files for + * + * @return the number of the with the given category */ - @GuardedBy("videoFileMap") - private final Map videoFileMap = new HashMap<>(); + public long getCategoryCount(Category cat) { + try { + return Case.getCurrentCase().getServices().getTagsManager().getContentTagsByTagName(cat.getTagName()).stream() + .map(ContentTag::getContent) + .map(Content::getId) + .filter(this::isInDB) + .count(); - public boolean isVideoFile(AbstractFile f) throws TskCoreException { - synchronized (videoFileMap) { - if (videoFileMap.containsKey(f.getId())) { - return videoFileMap.get(f.getId()); - } - - boolean isVideo = ImageGalleryModule.isVideoFile(f); - videoFileMap.put(f.getId(), isVideo); - return isVideo; + } catch (IllegalStateException ex) { + LOGGER.log(Level.WARNING, "Case closed while getting files"); + } catch (TskCoreException ex1) { + LOGGER.log(Level.SEVERE, "Failed to get content tags by tag name.", ex1); } + return -1; } /** diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java index c04cb2c900..78bb8c6401 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java @@ -68,7 +68,7 @@ public abstract class DrawableFile extends AbstractFile return new ImageFile<>(abstractFileById, analyzed); } } - + /** * Skip the database query if we have already determined the file type. */ @@ -78,7 +78,7 @@ public abstract class DrawableFile extends AbstractFile } else { return new ImageFile<>(abstractFileById, analyzed); } - } + } public static DrawableFile create(Long id, boolean analyzed) throws TskCoreException, IllegalStateException { @@ -105,7 +105,8 @@ public abstract class DrawableFile extends AbstractFile protected DrawableFile(T file, Boolean analyzed) { /* @TODO: the two 'new Integer(0).shortValue()' values and null are * placeholders because the super constructor expects values i can't get - * easily at the moment. I assume this is related to why ReadContentInputStream can't read from DrawableFiles.*/ + * easily at the moment. I assume this is related to why + * ReadContentInputStream can't read from DrawableFiles. */ super(file.getSleuthkitCase(), file.getId(), file.getAttrType(), file.getAttrId(), file.getName(), file.getType(), file.getMetaAddr(), (int) file.getMetaSeq(), file.getDirType(), file.getMetaType(), null, new Integer(0).shortValue(), file.getSize(), file.getCtime(), file.getCrtime(), file.getAtime(), file.getMtime(), new Integer(0).shortValue(), file.getUid(), file.getGid(), file.getMd5Hash(), file.getKnown(), file.getParentPath()); this.analyzed = new SimpleBooleanProperty(analyzed); @@ -115,9 +116,8 @@ public abstract class DrawableFile extends AbstractFile public abstract boolean isVideo(); - synchronized public Collection getHashHitSetNames() { - Collection hashHitSetNames = ImageGalleryController.getDefault().getDatabase().getHashSetsForFile(getId()); - return hashHitSetNames; + public Collection getHashHitSetNames() { + return ImageGalleryController.getDefault().getHashSetManager().getHashSetsForFile(getId()); } @Override @@ -290,10 +290,10 @@ public abstract class DrawableFile extends AbstractFile } } catch (TskCoreException ex) { Logger.getLogger(DrawableFile.class.getName()).log(Level.WARNING, "problem looking up category for file " + this.getName(), ex); - } catch (IllegalStateException ex){ + } catch (IllegalStateException ex) { // We get here many times if the case is closed during ingest, so don't print out a ton of warnings. } - + } public abstract Image getThumbnail(); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/HashSetManager.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/HashSetManager.java new file mode 100644 index 0000000000..1ca547c81e --- /dev/null +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/HashSetManager.java @@ -0,0 +1,71 @@ +package org.sleuthkit.autopsy.imagegallery.datamodel; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import java.util.Set; + +/** + * Manages a cache of hashset hits as a map from fileID to hashset names. + * Initial/invalid values are loaded from the backing DrawableDB + */ +public class HashSetManager { + + /** The db that initial values are loaded from. */ + private DrawableDB db = null; + + /** the internal cache from fileID to a set of hashset names. */ + private final LoadingCache> hashSetCache = CacheBuilder.newBuilder().build(CacheLoader.from(this::getHashSetsForFileHelper)); + + /** + * assign the given db to back this hashset manager. + * + * @param db + */ + public void setDb(DrawableDB db) { + this.db = db; + hashSetCache.invalidateAll(); + } + + /** + * helper method to load hashset hits for the given fileID from the db + * + * @param fileID + * + * @return the names of the hashsets the given fileID is in + */ + private Set getHashSetsForFileHelper(long fileID) { + return db.getHashSetsForFile(fileID); + } + + /** + * is the given fileID in any hashset + * + * @param fileID + * + * @return true if the file is in any hashset + */ + public boolean isInAnyHashSet(long fileID) { + return getHashSetsForFile(fileID).isEmpty() == false; + } + + /** + * get the names of the hash sets the given fileId is in + * + * @param fileID + * + * @return a set containging the names of the hash sets for the given file + */ + public Set getHashSetsForFile(long fileID) { + return hashSetCache.getUnchecked(fileID); + } + + /** + * invalidate the cached hashset names for the given fileID + * + * @param fileID the fileID to invalidate in the cache + */ + public void invalidateHashSetsForFile(long fileID) { + hashSetCache.invalidate(fileID); + } +} diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/grouping/DrawableGroup.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/grouping/DrawableGroup.java index 024ef12f8b..445b336ba9 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/grouping/DrawableGroup.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/grouping/DrawableGroup.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013 Basis Technology Corp. + * Copyright 2013-15 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -43,11 +43,11 @@ public class DrawableGroup implements Comparable { private final ObservableList fileIDs = FXCollections.observableArrayList(); //cache the number of files in this groups with hashset hits - private int hashSetHitsCount = -1; + private long hashSetHitsCount = -1; private final ReadOnlyBooleanWrapper seen = new ReadOnlyBooleanWrapper(false); synchronized public ObservableList fileIds() { - return fileIDs; + return FXCollections.unmodifiableObservableList(fileIDs); } final public GroupKey groupKey; @@ -82,29 +82,28 @@ public class DrawableGroup implements Comparable { } /** - * Call to indicate that an image has been added or removed from the group, - * so the hash counts may not longer be accurate. + * Call to indicate that an file has been added or removed from the group, + * so the hash counts may no longer be accurate. */ - synchronized public void invalidateHashSetHitsCount() { + synchronized private void invalidateHashSetHitsCount() { hashSetHitsCount = -1; } - synchronized public int getHashSetHitsCount() { - //TODO: use the drawable db for this ? -jm + /** + * @return the number of files in this group that have hash set hits + */ + synchronized public long getHashSetHitsCount() { if (hashSetHitsCount < 0) { - hashSetHitsCount = 0; - for (Long fileID : fileIds()) { - - try { - if (ImageGalleryController.getDefault().getDatabase().isInHashSet(fileID)) { - hashSetHitsCount++; - } - } catch (IllegalStateException | NullPointerException ex) { - LOGGER.log(Level.WARNING, "could not access case during getFilesWithHashSetHitsCount()"); - break; - } + try { + hashSetHitsCount = fileIDs.stream() + .map(fileID -> ImageGalleryController.getDefault().getHashSetManager().isInAnyHashSet(fileID)) + .filter(Boolean::booleanValue) + .count(); + } catch (IllegalStateException | NullPointerException ex) { + LOGGER.log(Level.WARNING, "could not access case during getFilesWithHashSetHitsCount()"); } } + return hashSetHitsCount; } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/grouping/GroupManager.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/grouping/GroupManager.java index 84f79dcfe4..1af4c4ea71 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/grouping/GroupManager.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/grouping/GroupManager.java @@ -78,7 +78,6 @@ public class GroupManager implements FileUpdateEvent.FileUpdateListener { private DrawableDB db; private final ImageGalleryController controller; - /** * map from {@link GroupKey}s to {@link DrawableGroup}s. All groups (even * not @@ -436,7 +435,7 @@ public class GroupManager implements FileUpdateEvent.FileUpdateListener { switch (groupBy.attrName) { //these cases get special treatment case CATEGORY: - values = (List) Category.valuesList(); + values = (List) Arrays.asList(Category.values()); break; case TAGS: values = (List) Case.getCurrentCase().getServices().getTagsManager().getTagNamesInUse().stream() @@ -447,7 +446,7 @@ public class GroupManager implements FileUpdateEvent.FileUpdateListener { values = (List) Arrays.asList(false, true); break; case HASHSET: - TreeSet names = new TreeSet<>((Set) db.getHashSetNames()); + TreeSet names = new TreeSet<>((Collection) db.getHashSetNames()); values = new ArrayList<>(names); break; default: @@ -457,8 +456,8 @@ public class GroupManager implements FileUpdateEvent.FileUpdateListener { return values; } catch (TskCoreException ex) { - LOGGER.log(Level.WARNING, "TSK error getting list of type " + groupBy.getDisplayName()); - return new ArrayList(); + LOGGER.log(Level.WARNING, "TSK error getting list of type {0}", groupBy.getDisplayName()); + return Collections.emptyList(); } } @@ -490,7 +489,7 @@ public class GroupManager implements FileUpdateEvent.FileUpdateListener { for (TagName tn : tns) { List contentTags = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByTagName(tn); for (ContentTag ct : contentTags) { - if (ct.getContent() instanceof AbstractFile && db.isDrawableFile(((AbstractFile) ct.getContent()).getId())) { + if (ct.getContent() instanceof AbstractFile && db.isInDB(ct.getContent().getId())) { files.add(ct.getContent().getId()); } } @@ -502,8 +501,7 @@ public class GroupManager implements FileUpdateEvent.FileUpdateListener { List files = new ArrayList<>(); List contentTags = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByTagName(category.getTagName()); for (ContentTag ct : contentTags) { - if (ct.getContent() instanceof AbstractFile && db.isDrawableFile(((AbstractFile) ct.getContent()).getId())) { - + if (ct.getContent() instanceof AbstractFile && db.isInDB(ct.getContent().getId())) { files.add(ct.getContent().getId()); } } @@ -516,27 +514,12 @@ public class GroupManager implements FileUpdateEvent.FileUpdateListener { } } - /** - * Count the number of files with the given category. - * This is faster than getFileIDsWithCategory and should be used if only the - * counts are needed and not the file IDs. - * - * @param category Category to match against - * - * @return Number of files with the given category - * - * @throws TskCoreException - */ - public int countFilesWithCategory(Category category) throws TskCoreException { - return db.getCategoryCount(category); - } - public List getFileIDsWithTag(TagName tagName) throws TskCoreException { try { List files = new ArrayList<>(); List contentTags = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByTagName(tagName); for (ContentTag ct : contentTags) { - if (ct.getContent() instanceof AbstractFile && db.isDrawableFile(((AbstractFile) ct.getContent()).getId())) { + if (ct.getContent() instanceof AbstractFile && db.isInDB(ct.getContent().getId())) { files.add(ct.getContent().getId()); } @@ -655,8 +638,6 @@ public class GroupManager implements FileUpdateEvent.FileUpdateListener { if (checkAnalyzed != null) { // => the group is analyzed, so add it to the ui populateAnalyzedGroup(gk, checkAnalyzed); } - } else { - g.invalidateHashSetHitsCount(); } } } @@ -677,7 +658,7 @@ public class GroupManager implements FileUpdateEvent.FileUpdateListener { */ for (final long fileId : fileIDs) { - db.updateHashSetsForFile(fileId); + controller.getHashSetManager().invalidateHashSetsForFile(fileId); //get grouping(s) this file would be in Set> groupsForFile = getGroupKeysForFileID(fileId); @@ -700,7 +681,7 @@ public class GroupManager implements FileUpdateEvent.FileUpdateListener { } //we fire this event for all files so that the category counts get updated during initial db population - Category.fireChange(fileIDs); + controller.getCategoryManager().fireChange(fileIDs); if (evt.getChangedAttribute() == DrawableAttribute.TAGS) { TagUtils.fireChange(fileIDs); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/DrawableTile.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/DrawableTile.java index e7dccedb53..7a9cba6011 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/DrawableTile.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/DrawableTile.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013 Basis Technology Corp. + * Copyright 2013-15 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,10 +18,9 @@ */ package org.sleuthkit.autopsy.imagegallery.gui; -import java.net.URL; import java.util.Objects; -import java.util.ResourceBundle; import java.util.logging.Level; +import javafx.application.Platform; import javafx.fxml.FXML; import javafx.scene.CacheHint; import javafx.scene.control.Control; @@ -34,17 +33,17 @@ import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.coreutils.ThreadConfined.ThreadType; import org.sleuthkit.autopsy.imagegallery.FXMLConstructor; import org.sleuthkit.autopsy.imagegallery.TagUtils; -import org.sleuthkit.autopsy.imagegallery.datamodel.Category; +import static org.sleuthkit.autopsy.imagegallery.gui.DrawableViewBase.globalSelectionModel; /** - * GUI component that represents a single image as a tile with an icon, a label + * GUI component that represents a single image as a tile with an icon, a label, * a color coded border and possibly other controls. Designed to be in a * {@link GroupPane}'s TilePane or SlideShow. * * * TODO: refactor this to extend from {@link Control}? -jm */ -public class DrawableTile extends SingleDrawableViewBase implements Category.CategoryListener, TagUtils.TagListener { +public class DrawableTile extends DrawableViewBase implements TagUtils.TagListener { private static final DropShadow LAST_SELECTED_EFFECT = new DropShadow(10, Color.BLUE); @@ -56,12 +55,6 @@ public class DrawableTile extends SingleDrawableViewBase implements Category.Cat @FXML private ImageView imageView; - @FXML - private ResourceBundle resources; - - @FXML - private URL location; - @Override protected void disposeContent() { //no-op @@ -99,11 +92,23 @@ public class DrawableTile extends SingleDrawableViewBase implements Category.Cat } @Override - @ThreadConfined(type = ThreadType.UI) + @ThreadConfined(type = ThreadType.JFX) protected void clearContent() { imageView.setImage(null); } + /** + * {@inheritDoc } + */ + @Override + protected void updateSelectionState() { + super.updateSelectionState(); + final boolean lastSelected = Objects.equals(globalSelectionModel.lastSelectedProperty().get(), fileID); + Platform.runLater(() -> { + setEffect(lastSelected ? LAST_SELECTED_EFFECT : null); + }); + } + @Override protected Runnable getContentUpdateRunnable() { Image image = file.getThumbnail(); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/DrawableView.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/DrawableView.java index 9de7704d7a..566fcb079b 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/DrawableView.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/DrawableView.java @@ -1,5 +1,6 @@ package org.sleuthkit.autopsy.imagegallery.gui; +import com.google.common.eventbus.Subscribe; import java.util.Collection; import java.util.logging.Level; import javafx.application.Platform; @@ -14,13 +15,17 @@ import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.imagegallery.TagUtils; import org.sleuthkit.autopsy.imagegallery.datamodel.Category; +import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryChangeEvent; +import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile; /** - * TODO: extract common interface out of {@link SingleImageView} and - * {@link MetaDataPane} + * Interface for classes that are views of a single DrawableFile. Implementation + * of DrawableView must be registered with {@link CategoryManager#registerListener(java.lang.Object) + * } to have there {@link DrawableView#handleCategoryChanged(org.sleuthkit.autopsy.imagegallery.datamodel.CategoryChangeEvent) + * } method invoked */ -public interface DrawableView extends Category.CategoryListener, TagUtils.TagListener { +public interface DrawableView extends TagUtils.TagListener { //TODO: do this all in css? -jm static final int CAT_BORDER_WIDTH = 10; @@ -51,16 +56,24 @@ public interface DrawableView extends Category.CategoryListener, TagUtils.TagLis Long getFileID(); - @Override - void handleCategoryChanged(Collection ids); + /** + * update the visual representation of the category of the assigned file. + * Implementations of {@link DrawableView} must register themselves with + * {@link CategoryManager#registerListener(java.lang.Object)} to ahve this + * method invoked + * + * @param evt the CategoryChangeEvent to handle + */ + @Subscribe + void handleCategoryChanged(CategoryChangeEvent evt); @Override void handleTagsChanged(Collection ids); default boolean hasHashHit() { - try{ + try { return getFile().getHashHitSetNames().isEmpty() == false; - } catch (NullPointerException ex){ + } catch (NullPointerException ex) { // I think this happens when we're in the process of removing images from the view while // also trying to update it? Logger.getLogger(DrawableView.class.getName()).log(Level.WARNING, "Error looking up hash set hits"); @@ -90,13 +103,12 @@ public interface DrawableView extends Category.CategoryListener, TagUtils.TagLis default Category updateCategoryBorder() { final Category category = getFile().getCategory(); final Border border = hasHashHit() && (category == Category.ZERO) - ? HASH_BORDER - : DrawableView.getCategoryBorder(category); + ? HASH_BORDER + : DrawableView.getCategoryBorder(category); Platform.runLater(() -> { getBorderable().setBorder(border); }); return category; } - } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SingleDrawableViewBase.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/DrawableViewBase.java similarity index 93% rename from ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SingleDrawableViewBase.java rename to ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/DrawableViewBase.java index 7d3a55a365..6792d6d38f 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SingleDrawableViewBase.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/DrawableViewBase.java @@ -19,6 +19,7 @@ */ package org.sleuthkit.autopsy.imagegallery.gui; +import com.google.common.eventbus.Subscribe; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -71,7 +72,7 @@ import org.sleuthkit.autopsy.imagegallery.TagUtils; import org.sleuthkit.autopsy.imagegallery.actions.AddDrawableTagAction; import org.sleuthkit.autopsy.imagegallery.actions.CategorizeAction; import org.sleuthkit.autopsy.imagegallery.actions.SwingMenuItemAdapter; -import org.sleuthkit.autopsy.imagegallery.datamodel.Category; +import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryChangeEvent; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile; import org.sleuthkit.autopsy.imagegallery.grouping.GroupKey; @@ -88,9 +89,9 @@ import org.sleuthkit.datamodel.TskCoreException; * of {@link DrawableView}s should implement the interface directly * */ -public abstract class SingleDrawableViewBase extends AnchorPane implements DrawableView { +public abstract class DrawableViewBase extends AnchorPane implements DrawableView { - private static final Logger LOGGER = Logger.getLogger(SingleDrawableViewBase.class.getName()); + private static final Logger LOGGER = Logger.getLogger(DrawableViewBase.class.getName()); private static final Border UNSELECTED_BORDER = new Border(new BorderStroke(Color.GRAY, BorderStrokeStyle.SOLID, new CornerRadii(2), new BorderWidths(3))); @@ -141,11 +142,11 @@ public abstract class SingleDrawableViewBase extends AnchorPane implements Drawa protected Long fileID; /** - * the groupPane this {@link SingleDrawableViewBase} is embedded in + * the groupPane this {@link DrawableViewBase} is embedded in */ protected GroupPane groupPane; - protected SingleDrawableViewBase() { + protected DrawableViewBase() { globalSelectionModel.getSelected().addListener((Observable observable) -> { updateSelectionState(); @@ -186,7 +187,7 @@ public abstract class SingleDrawableViewBase extends AnchorPane implements Drawa groupContextMenu.hide(); } contextMenu = buildContextMenu(); - contextMenu.show(SingleDrawableViewBase.this, t.getScreenX(), t.getScreenY()); + contextMenu.show(DrawableViewBase.this, t.getScreenX(), t.getScreenY()); break; } @@ -360,14 +361,14 @@ public abstract class SingleDrawableViewBase extends AnchorPane implements Drawa disposeContent(); if (this.fileID == null || Case.isCaseOpen() == false) { - Category.unregisterListener(this); + ImageGalleryController.getDefault().getCategoryManager().unregisterListener(this); TagUtils.unregisterListener(this); file = null; Platform.runLater(() -> { clearContent(); }); } else { - Category.registerListener(this); + ImageGalleryController.getDefault().getCategoryManager().registerListener(this); TagUtils.registerListener(this); getFile(); @@ -385,10 +386,14 @@ public abstract class SingleDrawableViewBase extends AnchorPane implements Drawa } } - private void updateSelectionState() { + /** + * update the visual representation of the selection state of this + * DrawableView + */ + protected void updateSelectionState() { final boolean selected = globalSelectionModel.isSelected(fileID); Platform.runLater(() -> { - SingleDrawableViewBase.this.setBorder(selected ? SELECTED_BORDER : UNSELECTED_BORDER); + setBorder(selected ? SELECTED_BORDER : UNSELECTED_BORDER); }); } @@ -397,9 +402,10 @@ public abstract class SingleDrawableViewBase extends AnchorPane implements Drawa return imageBorder; } + @Subscribe @Override - public void handleCategoryChanged(Collection ids) { - if (ids.contains(fileID)) { + public void handleCategoryChanged(CategoryChangeEvent evt) { + if (evt.getIds().contains(fileID)) { updateCategoryBorder(); } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/GroupPane.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/GroupPane.java index 46362fc0dc..ddca55e4a6 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/GroupPane.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/GroupPane.java @@ -85,6 +85,7 @@ import javafx.scene.paint.Color; import javafx.util.Duration; import javax.swing.Action; import javax.swing.SwingUtilities; +import org.apache.commons.lang3.StringUtils; import org.controlsfx.control.GridCell; import org.controlsfx.control.GridView; import org.controlsfx.control.SegmentedButton; @@ -123,8 +124,13 @@ import org.sleuthkit.datamodel.TskCoreException; * both a {@link GridView} based view and a {@link SlideShowView} view by * swapping out its internal components. * - * TODO: review for synchronization issues. TODO: Extract the The GridView - * instance to a separate class analogous to the SlideShow + * + * TODO: Extract the The GridView instance to a separate class analogous to the + * SlideShow. Move selection model into controlsfx GridView and submit pull + * request to them. + * https://bitbucket.org/controlsfx/controlsfx/issue/4/add-a-multipleselectionmodel-to-gridview + * + * */ public class GroupPane extends BorderPane implements GroupView { @@ -205,10 +211,8 @@ public class GroupPane extends BorderPane implements GroupView { * to determine whether fileIDs are visible or are offscreen. No entry * indicates the given fileID is not displayed on screen. DrawableCells are * responsible for adding and removing themselves from this map. - * - * TODO: use ConcurrentHashMap ? */ - @ThreadConfined(type = ThreadType.UI) + @ThreadConfined(type = ThreadType.JFX) private final Map cellMap = new HashMap<>(); private final InvalidationListener filesSyncListener = (observable) -> { @@ -298,7 +302,7 @@ public class GroupPane extends BorderPane implements GroupView { /** create the string to display in the group header */ protected String getHeaderString() { return isNull(getGrouping()) ? "" - : defaultIfBlank(getGrouping().getGroupByValueDislpayName(), DrawableGroup.getBlankGroupName()) + " -- " + : StringUtils.defaultIfBlank(getGrouping().getGroupByValueDislpayName(), DrawableGroup.getBlankGroupName()) + " -- " + getGrouping().getHashSetHitsCount() + " hash set hits / " + getGrouping().getSize() + " files"; } @@ -505,6 +509,7 @@ public class GroupPane extends BorderPane implements GroupView { } final ObservableList fileIds = gridView.getItems(); + int selectedIndex = fileIds.indexOf(newFileID); if (selectedIndex == -1) { //somehow we got passed a file id that isn't in the curent group. @@ -527,10 +532,6 @@ public class GroupPane extends BorderPane implements GroupView { .max().getAsInt(); //[minIndex, maxIndex] is the range of indexes in the fileIDs list that are currently displayed - if (minIndex < 0 && maxIndex < 0) { - return; - } - if (selectedIndex < minIndex) { scrollBar.decrement(); } else if (selectedIndex > maxIndex) { @@ -647,6 +648,7 @@ public class GroupPane extends BorderPane implements GroupView { } void resetItem() { + updateItem(null, true); tile.setFile(null); } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/MetaDataPane.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/MetaDataPane.java index b8cfe0aa4e..e5382173c4 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/MetaDataPane.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/MetaDataPane.java @@ -18,18 +18,15 @@ */ package org.sleuthkit.autopsy.imagegallery.gui; +import com.google.common.eventbus.Subscribe; import java.io.IOException; -import java.net.URL; import java.util.Arrays; import java.util.Collection; -import java.util.ResourceBundle; import java.util.logging.Level; import java.util.stream.Collectors; import javafx.application.Platform; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; @@ -46,20 +43,20 @@ import static javafx.scene.layout.Region.USE_COMPUTED_SIZE; import javafx.scene.text.Text; import javafx.util.Pair; import org.apache.commons.lang3.StringUtils; -import org.openide.util.Exceptions; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.TagUtils; import org.sleuthkit.autopsy.imagegallery.datamodel.Category; +import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryChangeEvent; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile; import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.TskCoreException; /** - * + * Shows details of the selected file. */ -public class MetaDataPane extends AnchorPane implements Category.CategoryListener, TagUtils.TagListener, DrawableView { +public class MetaDataPane extends AnchorPane implements TagUtils.TagListener, DrawableView { private static final Logger LOGGER = Logger.getLogger(MetaDataPane.class.getName()); @@ -70,12 +67,6 @@ public class MetaDataPane extends AnchorPane implements Category.CategoryListene @FXML private ImageView imageView; - @FXML - private ResourceBundle resources; - - @FXML - private URL location; - @FXML private TableColumn, ? extends Object>, DrawableAttribute> attributeColumn; @@ -102,7 +93,7 @@ public class MetaDataPane extends AnchorPane implements Category.CategoryListene assert tableView != null : "fx:id=\"tableView\" was not injected: check your FXML file 'MetaDataPane.fxml'."; assert valueColumn != null : "fx:id=\"valueColumn\" was not injected: check your FXML file 'MetaDataPane.fxml'."; TagUtils.registerListener(this); - Category.registerListener(this); + ImageGalleryController.getDefault().getCategoryManager().registerListener(this); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); tableView.setPlaceholder(new Label("Select a file to show its details here.")); @@ -131,7 +122,7 @@ public class MetaDataPane extends AnchorPane implements Category.CategoryListene .filter((String t) -> t.startsWith(Category.CATEGORY_PREFIX) == false) .collect(Collectors.joining(" ; ", "", ""))); } else { - return new SimpleStringProperty(StringUtils.join((Collection) p.getValue().getValue(), " ; ")); + return new SimpleStringProperty(StringUtils.join((Iterable) p.getValue().getValue(), " ; ")); } }); valueColumn.setPrefWidth(USE_COMPUTED_SIZE); @@ -192,15 +183,8 @@ public class MetaDataPane extends AnchorPane implements Category.CategoryListene try { file = controller.getFileFromId(fileID); updateUI(); - - file.categoryProperty().addListener(new ChangeListener() { - @Override - public void changed(ObservableValue ov, Category t, final Category t1) { - updateUI(); - } - }); } catch (TskCoreException ex) { - Exceptions.printStackTrace(ex); + LOGGER.log(Level.WARNING, "Failed to get drawable file from ID", ex); } } } @@ -236,9 +220,11 @@ public class MetaDataPane extends AnchorPane implements Category.CategoryListene return imageBorder; } + /** {@inheritDoc } */ + @Subscribe @Override - public void handleCategoryChanged(Collection ids) { - if (getFile() != null && ids.contains(getFileID())) { + public void handleCategoryChanged(CategoryChangeEvent evt) { + if (getFile() != null && evt.getIds().contains(getFileID())) { updateUI(); } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SlideShowView.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SlideShowView.java index a6cca03fe6..56811ae7a6 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SlideShowView.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SlideShowView.java @@ -64,7 +64,7 @@ import org.sleuthkit.datamodel.TskCoreException; * GroupPane. TODO: Extract a subclass for video files in slideshow mode-jm * TODO: reduce coupling to GroupPane */ -public class SlideShowView extends SingleDrawableViewBase implements TagUtils.TagListener, Category.CategoryListener { +public class SlideShowView extends DrawableViewBase implements TagUtils.TagListener { private static final Logger LOGGER = Logger.getLogger(SlideShowView.class.getName()); @@ -193,15 +193,17 @@ public class SlideShowView extends SingleDrawableViewBase implements TagUtils.Ta groupPane.grouping().addListener((Observable observable) -> { syncButtonVisibility(); - groupPane.getGrouping().fileIds().addListener((Observable observable1) -> { - syncButtonVisibility(); - }); + if (groupPane.getGrouping() != null) { + groupPane.getGrouping().fileIds().addListener((Observable observable1) -> { + syncButtonVisibility(); + }); + } }); } @ThreadConfined(type = ThreadType.ANY) private void syncButtonVisibility() { - try{ + try { final boolean hasMultipleFiles = groupPane.getGrouping().fileIds().size() > 1; Platform.runLater(() -> { rightButton.setVisible(hasMultipleFiles); @@ -209,7 +211,7 @@ public class SlideShowView extends SingleDrawableViewBase implements TagUtils.Ta rightButton.setManaged(hasMultipleFiles); leftButton.setManaged(hasMultipleFiles); }); - } catch (NullPointerException ex){ + } catch (NullPointerException ex) { // The case has likely been closed LOGGER.log(Level.WARNING, "Error accessing groupPane"); } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SummaryTablePane.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SummaryTablePane.java index 1e78e8945e..6692bf873f 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SummaryTablePane.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SummaryTablePane.java @@ -18,9 +18,8 @@ */ package org.sleuthkit.autopsy.imagegallery.gui; +import com.google.common.eventbus.Subscribe; import java.util.Arrays; -import java.util.Collection; -import java.util.logging.Level; import javafx.application.Platform; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; @@ -34,27 +33,26 @@ import static javafx.scene.layout.Region.USE_COMPUTED_SIZE; import javafx.scene.layout.VBox; import javafx.util.Pair; import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.imagegallery.FXMLConstructor; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.datamodel.Category; -import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryChangeEvent; /** * Displays summary statistics (counts) for each group */ -public class SummaryTablePane extends AnchorPane implements Category.CategoryListener { +public class SummaryTablePane extends AnchorPane { private static SummaryTablePane instance; @FXML - private TableColumn, String> catColumn; + private TableColumn, String> catColumn; @FXML - private TableColumn, Integer> countColumn; + private TableColumn, Long> countColumn; @FXML - private TableView> tableView; + private TableView> tableView; @FXML void initialize() { @@ -68,16 +66,16 @@ public class SummaryTablePane extends AnchorPane implements Category.CategoryLis tableView.prefHeightProperty().set(7 * 25); //set up columns - catColumn.setCellValueFactory((TableColumn.CellDataFeatures, String> p) -> new SimpleObjectProperty<>(p.getValue().getKey().getDisplayName())); + catColumn.setCellValueFactory((TableColumn.CellDataFeatures, String> p) -> new SimpleObjectProperty<>(p.getValue().getKey().getDisplayName())); catColumn.setPrefWidth(USE_COMPUTED_SIZE); - countColumn.setCellValueFactory((TableColumn.CellDataFeatures, Integer> p) -> new SimpleObjectProperty<>(p.getValue().getValue())); + countColumn.setCellValueFactory((TableColumn.CellDataFeatures, Long> p) -> new SimpleObjectProperty<>(p.getValue().getValue())); countColumn.setPrefWidth(USE_COMPUTED_SIZE); tableView.getColumns().setAll(Arrays.asList(catColumn, countColumn)); // //register for category events - Category.registerListener(this); + ImageGalleryController.getDefault().getCategoryManager().registerListener(this); } private SummaryTablePane() { @@ -94,16 +92,16 @@ public class SummaryTablePane extends AnchorPane implements Category.CategoryLis /** * listen to Category updates and rebuild the table */ - @Override - public void handleCategoryChanged(Collection ids) { - final ObservableList> data = FXCollections.observableArrayList(); + @Subscribe + public void handleCategoryChanged(CategoryChangeEvent evt) { + refresh(); + } + + public void refresh() { + final ObservableList> data = FXCollections.observableArrayList(); if (Case.isCaseOpen()) { for (Category cat : Category.values()) { - try { - data.add(new Pair<>(cat, ImageGalleryController.getDefault().getGroupManager().countFilesWithCategory(cat))); - } catch (TskCoreException ex) { - Logger.getLogger(SummaryTablePane.class.getName()).log(Level.WARNING, "Error performing category file count"); - } + data.add(new Pair<>(cat, ImageGalleryController.getDefault().getCategoryManager().getCategoryCount(cat))); } } Platform.runLater(() -> { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/Toolbar.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/Toolbar.java index 445854adee..bf62f89084 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/Toolbar.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/Toolbar.java @@ -18,9 +18,7 @@ */ package org.sleuthkit.autopsy.imagegallery.gui; -import java.net.URL; import java.util.ArrayList; -import java.util.ResourceBundle; import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.Observable; @@ -29,6 +27,7 @@ import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.event.ActionEvent; +import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; @@ -44,9 +43,10 @@ import javax.swing.SortOrder; import org.openide.util.Exceptions; import org.sleuthkit.autopsy.imagegallery.FXMLConstructor; import org.sleuthkit.autopsy.imagegallery.FileIDSelectionModel; -import org.sleuthkit.autopsy.imagegallery.ThumbnailCache; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.TagUtils; +import org.sleuthkit.autopsy.imagegallery.ThumbnailCache; +import org.sleuthkit.autopsy.imagegallery.actions.CategorizeAction; import org.sleuthkit.autopsy.imagegallery.datamodel.Category; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute; import org.sleuthkit.autopsy.imagegallery.grouping.GroupSortBy; @@ -54,18 +54,12 @@ import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.TskCoreException; /** - * Controller for the ToolBar + * Controller for the ToolBar */ public class Toolbar extends ToolBar { private static final int SIZE_SLIDER_DEFAULT = 100; - @FXML - private ResourceBundle resources; - - @FXML - private URL location; - @FXML private ComboBox> groupByBox; @@ -175,14 +169,14 @@ public class Toolbar extends ToolBar { } }); - catSelectedMenuButton.setOnAction(Category.FIVE.createSelCatMenuItem(catSelectedMenuButton).getOnAction()); + catSelectedMenuButton.setOnAction(createSelCatMenuItem(Category.FIVE, catSelectedMenuButton).getOnAction()); catSelectedMenuButton.setText(Category.FIVE.getDisplayName()); catSelectedMenuButton.setGraphic(new ImageView(DrawableAttribute.CATEGORY.getIcon())); catSelectedMenuButton.showingProperty().addListener((ObservableValue ov, Boolean t, Boolean t1) -> { if (t1) { ArrayList categoryMenues = new ArrayList<>(); for (final Category cat : Category.values()) { - MenuItem menuItem = cat.createSelCatMenuItem(catSelectedMenuButton); + MenuItem menuItem = createSelCatMenuItem(cat, catSelectedMenuButton); categoryMenues.add(menuItem); } catSelectedMenuButton.getItems().setAll(categoryMenues); @@ -230,4 +224,17 @@ public class Toolbar extends ToolBar { private Toolbar() { FXMLConstructor.construct(this, "Toolbar.fxml"); } + + private static MenuItem createSelCatMenuItem(Category cat, final SplitMenuButton catSelectedMenuButton) { + final MenuItem menuItem = new MenuItem(cat.getDisplayName(), new ImageView(DrawableAttribute.CATEGORY.getIcon())); + menuItem.setOnAction(new EventHandler() { + @Override + public void handle(ActionEvent t) { + new CategorizeAction().addTag(cat.getTagName(), ""); + catSelectedMenuButton.setText(cat.getDisplayName()); + catSelectedMenuButton.setOnAction(this); + } + }); + return menuItem; + } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/TreeNodeComparators.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/TreeNodeComparators.java index f16cc0cef6..096dd681a1 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/TreeNodeComparators.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/TreeNodeComparators.java @@ -28,31 +28,30 @@ import javafx.scene.control.TreeItem; enum TreeNodeComparators implements Comparator>, NonNullCompareable { ALPHABETICAL("Group Name") { - @Override - public int nonNullCompare(TreeItem o1, TreeItem o2) { - - return o1.getValue().getGroup().groupKey.getValue().toString().compareTo(o2.getValue().getGroup().groupKey.getValue().toString()); - } - }, HIT_COUNT("Hit Count") { - @Override - public int nonNullCompare(TreeItem o1, TreeItem o2) { - - return -Integer.compare(o1.getValue().getGroup().getHashSetHitsCount(), o2.getValue().getGroup().getHashSetHitsCount()); - } - }, FILE_COUNT("Group Size") { - @Override - public int nonNullCompare(TreeItem o1, TreeItem o2) { - - return -Integer.compare(o1.getValue().getGroup().getSize(), o2.getValue().getGroup().getSize()); - } - }, HIT_FILE_RATIO("Hit Density") { - @Override - public int nonNullCompare(TreeItem o1, TreeItem o2) { - - return -Double.compare(o1.getValue().getGroup().getHashSetHitsCount() / (double) o1.getValue().getGroup().getSize(), - o2.getValue().getGroup().getHashSetHitsCount() / (double) o2.getValue().getGroup().getSize()); - } - }; + @Override + public int nonNullCompare(TreeItem o1, TreeItem o2) { + return o1.getValue().getGroup().groupKey.getValue().toString().compareTo(o2.getValue().getGroup().groupKey.getValue().toString()); + } + }, + HIT_COUNT("Hit Count") { + @Override + public int nonNullCompare(TreeItem o1, TreeItem o2) { + return -Long.compare(o1.getValue().getGroup().getHashSetHitsCount(), o2.getValue().getGroup().getHashSetHitsCount()); + } + }, + FILE_COUNT("Group Size") { + @Override + public int nonNullCompare(TreeItem o1, TreeItem o2) { + return -Integer.compare(o1.getValue().getGroup().getSize(), o2.getValue().getGroup().getSize()); + } + }, + HIT_FILE_RATIO("Hit Density") { + @Override + public int nonNullCompare(TreeItem o1, TreeItem o2) { + return -Double.compare(o1.getValue().getGroup().getHashSetHitsCount() / (double) o1.getValue().getGroup().getSize(), + o2.getValue().getGroup().getHashSetHitsCount() / (double) o2.getValue().getGroup().getSize()); + } + }; @Override public int compare(TreeItem o1, TreeItem o2) { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/license-imageanalyzer.txt b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/license-imagegallery.txt similarity index 93% rename from ImageGallery/src/org/sleuthkit/autopsy/imagegallery/license-imageanalyzer.txt rename to ImageGallery/src/org/sleuthkit/autopsy/imagegallery/license-imagegallery.txt index fb3e1b684a..40b778ffd9 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/license-imageanalyzer.txt +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/license-imagegallery.txt @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-14 Basis Technology Corp. + * Copyright 2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License");