diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java index 497fa70c3a..c983e6a721 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java @@ -502,7 +502,7 @@ public final class ImageGalleryController { return file; } - public FileTask(AbstractFile f, DrawableDB taskDB) { + FileTask(AbstractFile f, DrawableDB taskDB) { super(); this.file = f; this.taskDB = taskDB; @@ -562,13 +562,13 @@ public final class ImageGalleryController { } } - @NbBundle.Messages({"BulkTask.committingDb.status=committing image/video database", - "BulkTask.stopCopy.status=Stopping copy to drawable db task.", - "BulkTask.errPopulating.errMsg=There was an error populating Image Gallery database."}) /** * Base abstract class for various methods of copying image files data, for * a given data source, into the Image gallery DB. */ + @NbBundle.Messages({"BulkTask.committingDb.status=committing image/video database", + "BulkTask.stopCopy.status=Stopping copy to drawable db task.", + "BulkTask.errPopulating.errMsg=There was an error populating Image Gallery database."}) abstract static class BulkTransferTask extends BackgroundTask { static private final String FILE_EXTENSION_CLAUSE @@ -581,15 +581,15 @@ public final class ImageGalleryController { + String.join("' OR mime_type LIKE '", FileTypeUtils.getAllSupportedMimeTypes()) //NON-NLS + "') "; - final String DRAWABLE_QUERY; - final String DATASOURCE_CLAUSE; + private final String DRAWABLE_QUERY; + private final String DATASOURCE_CLAUSE; - final ImageGalleryController controller; - final DrawableDB taskDB; - final SleuthkitCase tskCase; - final long dataSourceObjId; + protected final ImageGalleryController controller; + protected final DrawableDB taskDB; + protected final SleuthkitCase tskCase; + protected final long dataSourceObjId; - ProgressHandle progressHandle; + private ProgressHandle progressHandle; private boolean taskCompletionStatus; BulkTransferTask(long dataSourceObjId, ImageGalleryController controller) { @@ -612,11 +612,14 @@ public final class ImageGalleryController { } /** + * Do any cleanup for this task. * * @param success true if the transfer was successful */ abstract void cleanup(boolean success); + abstract void processFile(final AbstractFile f, DrawableDB.DrawableTransaction tr, CaseDbTransaction caseDBTransaction) throws TskCoreException; + /** * Gets a list of files to process. * @@ -628,8 +631,6 @@ public final class ImageGalleryController { return tskCase.findAllFilesWhere(DRAWABLE_QUERY); } - abstract void processFile(final AbstractFile f, DrawableDB.DrawableTransaction tr, CaseDbTransaction caseDBTransaction) throws TskCoreException; - @Override public void run() { progressHandle = getInitialProgressHandle(); @@ -775,16 +776,14 @@ public final class ImageGalleryController { * Copy files from a newly added data source into the DB. Get all "drawable" * files, based on extension and mime-type. After ingest we use file type id * module and if necessary jpeg/png signature matching to add/remove files - * - * TODO: create methods to simplify progress value/text updates to both - * netbeans and ImageGallery progress/status */ @NbBundle.Messages({"PrePopulateDataSourceFiles.committingDb.status=committing image/video database"}) static class PrePopulateDataSourceFiles extends BulkTransferTask { /** - * - * @param dataSourceId Data source object ID + * @param dataSourceObjId The object ID of the DataSource that is being + * pre-populated into the DrawableDB. + * @param controller The controller for this task. */ PrePopulateDataSourceFiles(long dataSourceObjId, ImageGalleryController controller) { super(dataSourceObjId, controller); @@ -805,5 +804,4 @@ public final class ImageGalleryController { return ProgressHandle.createHandle(Bundle.PrePopulateDataSourceFiles_prepopulatingDb_status(), this); } } - } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/NextUnseenGroup.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/NextUnseenGroup.java index 22f0fcb12b..b53c36c91e 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/NextUnseenGroup.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/NextUnseenGroup.java @@ -39,7 +39,8 @@ import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState; */ @NbBundle.Messages({ "NextUnseenGroup.markGroupSeen=Mark Group Seen", - "NextUnseenGroup.nextUnseenGroup=Next Unseen group"}) + "NextUnseenGroup.nextUnseenGroup=Next Unseen group", + "NextUnseenGroup.allGroupsSeen=All Groups Have Been Seen"}) public class NextUnseenGroup extends Action { private static final String IMAGE_PATH = "/org/sleuthkit/autopsy/imagegallery/images/"; //NON-NLS @@ -50,6 +51,7 @@ public class NextUnseenGroup extends Action { private static final String MARK_GROUP_SEEN = Bundle.NextUnseenGroup_markGroupSeen(); private static final String NEXT_UNSEEN_GROUP = Bundle.NextUnseenGroup_nextUnseenGroup(); + private static final String ALL_GROUPS_SEEN = Bundle.NextUnseenGroup_allGroupsSeen(); private final ImageGalleryController controller; private final ObservableList unSeenGroups; @@ -63,6 +65,7 @@ public class NextUnseenGroup extends Action { groupManager = controller.getGroupManager(); unSeenGroups = groupManager.getUnSeenGroups(); unSeenGroups.addListener((Observable observable) -> updateButton()); + controller.viewStateProperty().addListener((Observable observable) -> updateButton()); setEventHandler(event -> { //on fx-thread //if there is a group assigned to the view, mark it as seen @@ -88,16 +91,33 @@ public class NextUnseenGroup extends Action { private void updateButton() { int size = unSeenGroups.size(); - Platform.runLater(() -> { - setDisabled(size == 0); - if (size <= 1) { - setText(MARK_GROUP_SEEN); - setGraphic(new ImageView(END_IMAGE)); + if (size < 1) { + //there are no unseen groups. + Platform.runLater(() -> { + setDisabled(true); + setText(ALL_GROUPS_SEEN); + setGraphic(null); + }); + } else { + DrawableGroup get = unSeenGroups.get(0); + DrawableGroup orElse = Optional.ofNullable(controller.getViewState()).flatMap(GroupViewState::getGroup).orElse(null); + boolean equals = get.equals(orElse); + if (size == 1 & equals) { + //The only unseen group is the one that is being viewed. + Platform.runLater(() -> { + setDisabled(false); + setText(MARK_GROUP_SEEN); + setGraphic(new ImageView(END_IMAGE)); + }); } else { - setText(NEXT_UNSEEN_GROUP); - setGraphic(new ImageView(ADVANCE_IMAGE)); + //there are more unseen groups. + Platform.runLater(() -> { + setDisabled(false); + setText(NEXT_UNSEEN_GROUP); + setGraphic(new ImageView(ADVANCE_IMAGE)); + }); } - }); + } } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java index 472950e893..f779978dfb 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java @@ -39,7 +39,9 @@ import java.util.Map; import static java.util.Objects.isNull; import static java.util.Objects.nonNull; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Level; @@ -457,6 +459,7 @@ public final class DrawableDB { + " value VARCHAR(255) not null, " //NON-NLS + " attribute VARCHAR(255) not null, " //NON-NLS + " UNIQUE(data_source_obj_id, value, attribute) )"; //NON-NLS + tskCase.getCaseDbAccessManager().createTable(GROUPS_TABLENAME, tableSchema); } catch (TskCoreException ex) { logger.log(Level.SEVERE, "problem creating groups table", ex); //NON-NLS @@ -478,7 +481,7 @@ public final class DrawableDB { tskCase.getCaseDbAccessManager().createTable(GROUPS_SEEN_TABLENAME, tableSchema); } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "problem creating groups_seen table", ex); //NON-NLS + logger.log(Level.SEVERE, "problem creating image_gallery_groups_seen table", ex); //NON-NLS return false; } @@ -626,6 +629,26 @@ public final class DrawableDB { return names; } + static private String getGroupIdQuery(GroupKey groupKey) { + // query to find the group id from attribute/value + return String.format(" SELECT group_id FROM " + GROUPS_TABLENAME + + " WHERE attribute = \'%s\' AND value = \'%s\' AND data_source_obj_id = %d", + groupKey.getAttribute().attrName.toString(), + groupKey.getValueDisplayName(), + (groupKey.getAttribute() == DrawableAttribute.PATH) ? groupKey.getDataSourceObjId() : 0); + } + + /** + * Returns true if the specified group has been any examiner + * + * @param groupKey + * + * @return + */ + public boolean isGroupSeen(GroupKey groupKey) { + return isGroupSeenByExaminer(groupKey, -1); + } + /** * Returns true if the specified group has been seen by the specified * examiner @@ -638,96 +661,34 @@ public final class DrawableDB { public boolean isGroupSeenByExaminer(GroupKey groupKey, long examinerId) { // Callback to process result of seen query - class GroupSeenQueryResultProcessor implements CaseDbAccessQueryCallback { - - private boolean seen = false; - - boolean getGroupSeen() { - return seen; - } + class GroupSeenQueryResultProcessor extends CompletableFuture implements CaseDbAccessQueryCallback { @Override public void process(ResultSet resultSet) { try { if (resultSet != null) { while (resultSet.next()) { - seen = resultSet.getBoolean("seen"); //NON-NLS; + complete(resultSet.getInt("count") > 0); //NON-NLS; return; } } } catch (SQLException ex) { - logger.log(Level.SEVERE, "failed to get group seen", ex); //NON-NLS + logger.log(Level.SEVERE, "Failed to get group seen", ex); //NON-NLS } } } - try { - - // query to find the group id from attribute/value - String groupIdQuery = String.format("( SELECT group_id FROM " + GROUPS_TABLENAME - + " WHERE attribute = \'%s\' AND value = \'%s\' )", groupKey.getAttribute().attrName.toString(), groupKey.getValueDisplayName()); - - String groupSeenQueryStmt = String.format("seen FROM " + GROUPS_SEEN_TABLENAME + " WHERE examiner_id = %d AND group_id in ( %s )", examinerId, groupIdQuery); - GroupSeenQueryResultProcessor queryResultProcessor = new GroupSeenQueryResultProcessor(); - - tskCase.getCaseDbAccessManager().select(groupSeenQueryStmt, queryResultProcessor); - return queryResultProcessor.getGroupSeen(); - } catch (TskCoreException ex) { - String msg = String.format("Failed to get is group seen for group key %s", groupKey.getValueDisplayName()); //NON-NLS - logger.log(Level.WARNING, msg, ex); - } - - return false; - } - - /** - * Returns true if the specified group has been any examiner - * - * @param groupKey - * - * @return - */ - public boolean isGroupSeen(GroupKey groupKey) { - // Callback to process result of seen query - class GroupSeenQueryResultProcessor implements CaseDbAccessQueryCallback { - - private boolean seen = false; - - boolean getGroupSeen() { - return seen; - } - - @Override - public void process(ResultSet resultSet) { - try { - if (resultSet != null) { - while (resultSet.next()) { - int count = resultSet.getInt("count"); - seen = count > 0; - return; - } - } - } catch (SQLException ex) { - logger.log(Level.SEVERE, "failed to get group seen", ex); //NON-NLS - } - } - } + GroupSeenQueryResultProcessor queryResultProcessor = new GroupSeenQueryResultProcessor(); try { - - // query to find the group id from attribute/value - String groupIdQuery = ""; - - groupIdQuery = String.format("( SELECT group_id FROM " + GROUPS_TABLENAME - + " WHERE attribute = \'%s\' AND value = \'%s\' AND data_source_obj_id = %d)", groupKey.getAttribute().attrName.toString(), groupKey.getValueDisplayName(), (groupKey.getAttribute() == DrawableAttribute.PATH) ? groupKey.getDataSourceObjId() : 0); - - String groupSeenQueryStmt = String.format("COUNT((*) as count FROM " + GROUPS_SEEN_TABLENAME + " WHERE seen = 1 AND group_id in ( %s )", groupIdQuery); - - GroupSeenQueryResultProcessor queryResultProcessor = new GroupSeenQueryResultProcessor(); + String groupSeenQueryStmt = "COUNT(*) as count FROM " + GROUPS_SEEN_TABLENAME + + " WHERE seen = 1 " + + " AND group_id in ( " + getGroupIdQuery(groupKey) + ")" + + (examinerId > 0 ? " AND examiner_id = " + examinerId : "");// query to find the group id from attribute/value tskCase.getCaseDbAccessManager().select(groupSeenQueryStmt, queryResultProcessor); - return queryResultProcessor.getGroupSeen(); - } catch (TskCoreException ex) { + return queryResultProcessor.get(); + } catch (ExecutionException | InterruptedException | TskCoreException ex) { String msg = String.format("Failed to get is group seen for group key %s", groupKey.getValueDisplayName()); //NON-NLS logger.log(Level.WARNING, msg, ex); } @@ -746,6 +707,7 @@ public final class DrawableDB { * @throws TskCoreException */ public void markGroupSeen(GroupKey groupKey, boolean seen, long examinerID) throws TskCoreException { + // query to find the group id from attribute/value String innerQuery = String.format("( SELECT group_id FROM " + GROUPS_TABLENAME + " WHERE attribute = \'%s\' AND value = \'%s\' and data_source_obj_id = %d )", @@ -760,6 +722,7 @@ public final class DrawableDB { } tskCase.getCaseDbAccessManager().insertOrUpdate(GROUPS_SEEN_TABLENAME, insertSQL); + } public boolean removeFile(long id) { @@ -1175,7 +1138,7 @@ public final class DrawableDB { (A) results.getObject(groupBy.attrName.toString())); } } catch (SQLException ex) { - if (ex.getCause() instanceof java.lang.InterruptedException) { + if (!(ex.getCause() instanceof java.lang.InterruptedException)) { /* It seems like this originaly comes out of c3p0 when * its thread is intereupted (cancelled because of @@ -1185,7 +1148,6 @@ public final class DrawableDB { * see * https://sourceforge.net/p/c3p0/mailman/c3p0-users/thread/EBB32BB8-6487-43AF-B291-9464C9051869@mchange.com/ */ - } else { throw new TskCoreException("Unable to get values for attribute", ex); //NON-NLS } } catch (TskDataException ex) { @@ -1225,7 +1187,6 @@ public final class DrawableDB { if (DbType.POSTGRESQL == tskCase.getDatabaseType()) { insertSQL += "ON CONFLICT DO NOTHING"; } - tskCase.getCaseDbAccessManager().insert(GROUPS_TABLENAME, insertSQL, caseDbTransaction); } catch (TskCoreException ex) { // Don't need to report it if the case was closed diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/DrawableGroup.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/DrawableGroup.java index 64a73d02b8..224ac378c6 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/DrawableGroup.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/DrawableGroup.java @@ -26,6 +26,7 @@ import java.util.logging.Level; import javafx.beans.binding.Bindings; import javafx.beans.binding.DoubleBinding; import javafx.beans.binding.IntegerBinding; +import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.ReadOnlyLongProperty; import javafx.beans.property.ReadOnlyLongWrapper; @@ -165,8 +166,8 @@ public class DrawableGroup implements Comparable { return seen.get(); } - public ReadOnlyBooleanWrapper seenProperty() { - return seen; + public ReadOnlyBooleanProperty seenProperty() { + return seen.getReadOnlyProperty(); } @Subscribe 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 6bc735864b..95d00ee84a 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java @@ -46,6 +46,7 @@ import java.util.logging.Level; import java.util.regex.Pattern; import java.util.stream.Collectors; import javafx.application.Platform; +import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; @@ -59,7 +60,6 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import javax.swing.SortOrder; import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; -import org.apache.commons.lang3.ObjectUtils; import static org.apache.commons.lang3.ObjectUtils.notEqual; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.concurrent.BasicThreadFactory; @@ -99,7 +99,7 @@ public class GroupManager { /** An executor to submit async UI related background tasks to. */ private final ListeningExecutorService exec = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor( - new BasicThreadFactory.Builder().namingPattern("GUI Task -%d").build())); //NON-NLS + new BasicThreadFactory.Builder().namingPattern("GroupManager BG Thread-%d").build())); //NON-NLS private final ImageGalleryController controller; @@ -127,6 +127,7 @@ public class GroupManager { private final ReadOnlyObjectWrapper< DrawableAttribute> groupByProp = new ReadOnlyObjectWrapper<>(DrawableAttribute.PATH); private final ReadOnlyObjectWrapper sortOrderProp = new ReadOnlyObjectWrapper<>(SortOrder.ASCENDING); private final ReadOnlyObjectWrapper dataSourceProp = new ReadOnlyObjectWrapper<>(null);//null indicates all datasources + private final ReadOnlyBooleanWrapper collaborativeModeProp = new ReadOnlyBooleanWrapper(false); private final GroupingService regrouper; @@ -241,23 +242,24 @@ public class GroupManager { * * @return A ListenableFuture that encapsulates saving the seen state to the * DB. + * + * */ public ListenableFuture markGroupSeen(DrawableGroup group, boolean seen) { return exec.submit(() -> { try { Examiner examiner = controller.getSleuthKitCase().getCurrentExaminer(); - getDrawableDB().markGroupSeen(group.getGroupKey(), seen, examiner.getId()); group.setSeen(seen); - updateUnSeenGroups(group, seen); + updateUnSeenGroups(group); } catch (TskCoreException ex) { logger.log(Level.SEVERE, "Error marking group as seen", ex); //NON-NLS } }); } - synchronized private void updateUnSeenGroups(DrawableGroup group, boolean seen) { - if (seen) { + synchronized private void updateUnSeenGroups(DrawableGroup group) { + if (group.isSeen()) { unSeenGroups.removeAll(group); } else if (unSeenGroups.contains(group) == false) { unSeenGroups.add(group); @@ -579,14 +581,14 @@ public class GroupManager { try { Set fileIDs = getFileIDsInGroup(groupKey); if (Objects.nonNull(fileIDs)) { + + long examinerID = collaborativeModeProp.get() ? -1 : controller.getSleuthKitCase().getCurrentExaminer().getId(); + final boolean groupSeen = getDrawableDB().isGroupSeenByExaminer(groupKey, examinerID); DrawableGroup group; - Examiner examiner = controller.getSleuthKitCase().getCurrentExaminer(); - - final boolean groupSeen = getDrawableDB().isGroupSeenByExaminer(groupKey, examiner.getId()); if (groupMap.containsKey(groupKey)) { group = groupMap.get(groupKey); - group.setFiles(ObjectUtils.defaultIfNull(fileIDs, Collections.emptySet())); + group.setFiles(fileIDs); group.setSeen(groupSeen); } else { group = new DrawableGroup(groupKey, fileIDs, groupSeen); @@ -598,10 +600,9 @@ public class GroupManager { analyzedGroups.add(group); sortAnalyzedGroups(); } - updateUnSeenGroups(group, groupSeen); + updateUnSeenGroups(group); return group; - } } catch (TskCoreException ex) { logger.log(Level.SEVERE, "failed to get files for group: " + groupKey.getAttribute().attrName.toString() + " = " + groupKey.getValue(), ex); //NON-NLS @@ -634,9 +635,34 @@ public class GroupManager { } } + synchronized public void setCollaborativeMode(Boolean newValue) { + collaborativeModeProp.set(newValue); + analyzedGroups.forEach(group -> { + try { + boolean groupSeenByExaminer = getDrawableDB().isGroupSeenByExaminer( + group.getGroupKey(), + newValue ? -1 : controller.getSleuthKitCase().getCurrentExaminer().getId() + ); + group.setSeen(groupSeenByExaminer); + updateUnSeenGroups(group); + if (group.isSeen()) { + unSeenGroups.removeAll(group); + } else if (unSeenGroups.contains(group) == false) { + unSeenGroups.add(group); + } + + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error checking seen state of group.", ex); + } + }); + sortUnseenGroups(); + } + /** * Task to query database for files in sorted groups and build * DrawableGroups for them. + * + * @param The type of the values that this task will group by. */ @SuppressWarnings({"unchecked", "rawtypes"}) @NbBundle.Messages({"# {0} - groupBy attribute Name", diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java.orig b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java.orig deleted file mode 100644 index 668ef7baa4..0000000000 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java.orig +++ /dev/null @@ -1,888 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2013-18 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.grouping; - -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; -import com.google.common.eventbus.Subscribe; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; -import java.sql.ResultSet; -import java.sql.SQLException; -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; -import java.util.Map; -import java.util.Objects; -import static java.util.Objects.isNull; -import static java.util.Objects.nonNull; -import java.util.Optional; -import java.util.Set; -import java.util.TreeSet; -import java.util.concurrent.CancellationException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; -import java.util.logging.Level; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javafx.application.Platform; -import javafx.beans.property.ReadOnlyDoubleProperty; -import javafx.beans.property.ReadOnlyDoubleWrapper; -import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.ReadOnlyObjectWrapper; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import static javafx.concurrent.Worker.State.CANCELLED; -import static javafx.concurrent.Worker.State.FAILED; -import static javafx.concurrent.Worker.State.READY; -import static javafx.concurrent.Worker.State.RUNNING; -import static javafx.concurrent.Worker.State.SCHEDULED; -import static javafx.concurrent.Worker.State.SUCCEEDED; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; -import javax.swing.SortOrder; -import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.concurrent.BasicThreadFactory; -import org.netbeans.api.progress.ProgressHandle; -import org.openide.util.Exceptions; -import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; -import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; -import org.sleuthkit.autopsy.coreutils.LoggedTask; -import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.datamodel.DhsImageCategory; -import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; -import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager; -import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute; -import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableDB; -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.DataSource; -import org.sleuthkit.datamodel.SleuthkitCase; -import org.sleuthkit.datamodel.TagName; -import org.sleuthkit.datamodel.TskCoreException; -import org.sleuthkit.datamodel.TskData.DbType; -import org.sleuthkit.datamodel.TskDataException; - -/** - * Provides an abstraction layer on top of DrawableDB ( and to some extent - * SleuthkitCase ) to facilitate creation, retrieval, updating, and sorting of - * DrawableGroups. - */ -public class GroupManager { - - private static final Logger logger = Logger.getLogger(GroupManager.class.getName()); - - /** An executor to submit async UI related background tasks to. */ - private final ListeningExecutorService exec = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor( - new BasicThreadFactory.Builder().namingPattern("GUI Task -%d").build())); //NON-NLS - - private final ImageGalleryController controller; - - /** list of all analyzed groups */ - @GuardedBy("this") - private final ObservableList analyzedGroups = FXCollections.observableArrayList(); - private final ObservableList unmodifiableAnalyzedGroups = FXCollections.unmodifiableObservableList(analyzedGroups); - - /** list of unseen groups */ - @GuardedBy("this") - private final ObservableList unSeenGroups = FXCollections.observableArrayList(); - private final ObservableList unmodifiableUnSeenGroups = FXCollections.unmodifiableObservableList(unSeenGroups); - /** - * map from GroupKey} to DrawableGroupSs. All groups (even not fully - * analyzed or not visible groups could be in this map - */ - @GuardedBy("this") - private final Map, DrawableGroup> groupMap = new HashMap<>(); - - @GuardedBy("this") - private ReGroupTask groupByTask; - - /* - * --- current grouping/sorting attributes --- - */ - @GuardedBy("this") - private final ReadOnlyObjectWrapper< GroupSortBy> sortByProp = new ReadOnlyObjectWrapper<>(GroupSortBy.PRIORITY); - private final ReadOnlyObjectWrapper< DrawableAttribute> groupByProp = new ReadOnlyObjectWrapper<>(DrawableAttribute.PATH); - private final ReadOnlyObjectWrapper sortOrderProp = new ReadOnlyObjectWrapper<>(SortOrder.ASCENDING); - private final ReadOnlyObjectWrapper dataSourceProp = new ReadOnlyObjectWrapper<>(null);//null indicates all datasources - - private final ReadOnlyDoubleWrapper regroupProgress = new ReadOnlyDoubleWrapper(); - -<<<<<<< HEAD - synchronized DrawableDB getDB() { -======= - public void setDB(DrawableDB db) { - regroup(dataSource, groupBy, sortBy, sortOrder, true); - } - - private DrawableDB getDB() { ->>>>>>> 1010/7-datasource-filtering-test - return controller.getDatabase(); - } - - @SuppressWarnings("ReturnOfCollectionOrArrayField") - public ObservableList getAnalyzedGroups() { - return unmodifiableAnalyzedGroups; - } - - @SuppressWarnings("ReturnOfCollectionOrArrayField") - public ObservableList getUnSeenGroups() { - return unmodifiableUnSeenGroups; - } - - /** - * construct a group manager hooked up to the given db and controller - * - * @param controller - */ - public GroupManager(ImageGalleryController controller) { - this.controller = controller; - } - - /** - * Using the current groupBy set for this manager, find groupkeys for all - * the groups the given file is a part of - * - * @param file - * - * - * @return A a set of GroupKeys representing the group(s) the given file is - * a part of. - */ - @SuppressWarnings({"rawtypes", "unchecked"}) - synchronized public Set> getGroupKeysForFile(DrawableFile file) { - Set> resultSet = new HashSet<>(); - for (Comparable val : getGroupBy().getValue(file)) { - if (getGroupBy() == DrawableAttribute.TAGS) { - if (CategoryManager.isNotCategoryTagName((TagName) val)) { - resultSet.add(new GroupKey(getGroupBy(), val, getDataSource())); - } - } else { - resultSet.add(new GroupKey(getGroupBy(), val, getDataSource())); - } - } - return resultSet; - } - - /** - * Using the current grouping paramaters set for this manager, find - * GroupKeys for all the Groups the given file is a part of. - * - * @param fileID The Id of the file to get group keys for. - * - * @return A set of GroupKeys representing the group(s) the given file is a - * part of - */ - synchronized public Set> getGroupKeysForFileID(Long fileID) { - try { - DrawableDB db = getDB(); - if (nonNull(db)) { - DrawableFile file = db.getFileFromID(fileID); - return getGroupKeysForFile(file); - } else { - Logger.getLogger(GroupManager.class.getName()).log(Level.WARNING, "Failed to load file with id: {0} from database. There is no database assigned.", fileID); //NON-NLS - } - } catch (TskCoreException ex) { - Logger.getLogger(GroupManager.class.getName()).log(Level.SEVERE, "failed to load file with id: " + fileID + " from database", ex); //NON-NLS - } - return Collections.emptySet(); - } - - /** - * @param groupKey - * - * @return return the DrawableGroup (if it exists) for the given GroupKey, - * or null if no group exists for that key. - */ - @Nullable - synchronized public DrawableGroup getGroupForKey(@Nonnull GroupKey groupKey) { - return groupMap.get(groupKey); - } - - synchronized public void reset() { - if (groupByTask != null) { - groupByTask.cancel(true); - } - setSortBy(GroupSortBy.GROUP_BY_VALUE); - setGroupBy(DrawableAttribute.PATH); - setSortOrder(SortOrder.ASCENDING); - setDataSource(null); - - unSeenGroups.forEach(controller.getCategoryManager()::unregisterListener); - unSeenGroups.clear(); - analyzedGroups.forEach(controller.getCategoryManager()::unregisterListener); - analyzedGroups.clear(); - - groupMap.values().forEach(controller.getCategoryManager()::unregisterListener); - groupMap.clear(); - } - - synchronized public boolean isRegrouping() { - if (groupByTask == null) { - return false; - } - - switch (groupByTask.getState()) { - case READY: - case RUNNING: - case SCHEDULED: - return true; - case CANCELLED: - case FAILED: - case SUCCEEDED: - default: - return false; - } - } - - /** - * 'Save' the given group as seen in the drawable db. - * - * @param group The DrawableGroup to mark as seen. - * @param seen The seen state to set for the given group. - * - * @return A ListenableFuture that encapsulates saving the seen state to the - * DB. - */ - synchronized public ListenableFuture setGroupSeen(DrawableGroup group, boolean seen) { - DrawableDB db = getDB(); - if (nonNull(db)) { - return exec.submit(() -> { - try { - - db.setGroupSeen(group.getGroupKey(), seen); - group.setSeen(seen); - updateUnSeenGroups(group, seen); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error marking group as seen", ex); //NON-NLS - } - }); - } - - return Futures.immediateFuture(null); - } - - synchronized private void updateUnSeenGroups(DrawableGroup group, boolean seen) { - if (seen) { - unSeenGroups.removeAll(group); - } else if (unSeenGroups.contains(group) == false) { - unSeenGroups.add(group); - } - sortUnseenGroups(); - } - - /** - * remove the given file from the group with the given key. If the group - * doesn't exist or doesn't already contain this file, this method is a - * no-op - * - * @param groupKey the value of groupKey - * @param fileID the value of file - * - * @return The DrawableGroup the file was removed from. - * - */ - public synchronized DrawableGroup removeFromGroup(GroupKey groupKey, final Long fileID) { - //get grouping this file would be in - final DrawableGroup group = getGroupForKey(groupKey); - if (group != null) { - synchronized (group) { - group.removeFile(fileID); - - // If we're grouping by category, we don't want to remove empty groups. - if (groupKey.getAttribute() != DrawableAttribute.CATEGORY) { - if (group.getFileIDs().isEmpty()) { - if (analyzedGroups.contains(group)) { - analyzedGroups.remove(group); - sortAnalyzedGroups(); - } - if (unSeenGroups.contains(group)) { - unSeenGroups.remove(group); - sortUnseenGroups(); - } - - } - } - return group; - } - } else { //group == null - // It may be that this was the last unanalyzed file in the group, so test - // whether the group is now fully analyzed. - return popuplateIfAnalyzed(groupKey, null); - } - } - - synchronized private void sortUnseenGroups() { - FXCollections.sort(unSeenGroups, makeGroupComparator(getSortOrder(), getSortBy())); - } - - synchronized private void sortAnalyzedGroups() { - FXCollections.sort(analyzedGroups, makeGroupComparator(getSortOrder(), getSortBy())); - } - - synchronized public Set getFileIDsInGroup(GroupKey groupKey) throws TskCoreException { - Set fileIDsToReturn = Collections.emptySet(); - switch (groupKey.getAttribute().attrName) { - //these cases get special treatment - case CATEGORY: - fileIDsToReturn = getFileIDsWithCategory((DhsImageCategory) groupKey.getValue()); - break; - case TAGS: - fileIDsToReturn = getFileIDsWithTag((TagName) groupKey.getValue()); - break; - case MIME_TYPE: - fileIDsToReturn = getFileIDsWithMimeType((String) groupKey.getValue()); - break; -// case HASHSET: //comment out this case to use db functionality for hashsets -// return getFileIDsWithHashSetName((String) groupKey.getValue()); - default: - DrawableDB db = getDB(); - //straight db query - if (nonNull(db)) { - fileIDsToReturn = db.getFileIDsInGroup(groupKey); - } - } - return fileIDsToReturn; - } - - // @@@ This was kind of slow in the profiler. Maybe we should cache it. - // Unless the list of file IDs is necessary, use countFilesWithCategory() to get the counts. - synchronized public Set getFileIDsWithCategory(DhsImageCategory category) throws TskCoreException { - Set fileIDsToReturn = Collections.emptySet(); - DrawableDB db = getDB(); - if (nonNull(db)) { - try { - final DrawableTagsManager tagsManager = controller.getTagsManager(); - if (category == DhsImageCategory.ZERO) { - List< TagName> tns = Stream.of(DhsImageCategory.ONE, DhsImageCategory.TWO, - DhsImageCategory.THREE, DhsImageCategory.FOUR, DhsImageCategory.FIVE) - .map(tagsManager::getTagName) - .collect(Collectors.toList()); - - Set files = new HashSet<>(); - for (TagName tn : tns) { - if (tn != null) { - List contentTags = tagsManager.getContentTagsByTagName(tn); - files.addAll(contentTags.stream() - .filter(ct -> ct.getContent() instanceof AbstractFile) - .filter(ct -> db.isInDB(ct.getContent().getId())) - .map(ct -> ct.getContent().getId()) - .collect(Collectors.toSet())); - } - } - - fileIDsToReturn = db.findAllFileIdsWhere("obj_id NOT IN (" + StringUtils.join(files, ',') + ")"); //NON-NLS - } else { - - List contentTags = tagsManager.getContentTagsByTagName(tagsManager.getTagName(category)); - fileIDsToReturn = contentTags.stream() - .filter(ct -> ct.getContent() instanceof AbstractFile) - .filter(ct -> db.isInDB(ct.getContent().getId())) - .map(ct -> ct.getContent().getId()) - .collect(Collectors.toSet()); - } - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "TSK error getting files in Category:" + category.getDisplayName(), ex); //NON-NLS - throw ex; - } - } - return fileIDsToReturn; - } - - synchronized public Set getFileIDsWithTag(TagName tagName) throws TskCoreException { - Set files = new HashSet<>(); - try { - - List contentTags = controller.getTagsManager().getContentTagsByTagName(tagName); - DrawableDB db = getDB(); - for (ContentTag ct : contentTags) { - if (ct.getContent() instanceof AbstractFile && nonNull(db) && db.isInDB(ct.getContent().getId())) { - files.add(ct.getContent().getId()); - } - } - return files; - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "TSK error getting files with Tag:" + tagName.getDisplayName(), ex); //NON-NLS - throw ex; - } - } - - public synchronized GroupSortBy getSortBy() { - return sortByProp.get(); - } - - synchronized void setSortBy(GroupSortBy sortBy) { - sortByProp.set(sortBy); - } - - public ReadOnlyObjectProperty< GroupSortBy> getSortByProperty() { - return sortByProp.getReadOnlyProperty(); - } - - public synchronized DrawableAttribute getGroupBy() { - return groupByProp.get(); - } - - synchronized void setGroupBy(DrawableAttribute groupBy) { - groupByProp.set(groupBy); - } - - public ReadOnlyObjectProperty> getGroupByProperty() { - return groupByProp.getReadOnlyProperty(); - } - - public synchronized SortOrder getSortOrder() { - return sortOrderProp.get(); - } - - synchronized void setSortOrder(SortOrder sortOrder) { - sortOrderProp.set(sortOrder); - } - - public ReadOnlyObjectProperty getSortOrderProperty() { - return sortOrderProp.getReadOnlyProperty(); - } - - public synchronized DataSource getDataSource() { - return dataSourceProp.get(); - } - - synchronized void setDataSource(DataSource dataSource) { - dataSourceProp.set(dataSource); - } - - public ReadOnlyObjectProperty getDataSourceProperty() { - return dataSourceProp.getReadOnlyProperty(); - } - - /** - * Regroup all files in the database. see ReGroupTask for more details. - * - * @param The type of the values of the groupBy attriubte. - * @param dataSource The DataSource to show. Null for all data sources. - * @param groupBy The DrawableAttribute to group by - * @param sortBy The GroupSortBy to sort the groups by - * @param sortOrder The SortOrder to use when sorting the groups. - * @param force true to force a full db query regroup, even if only the - * sorting has changed. - */ - public synchronized > void regroup(DataSource dataSource, DrawableAttribute groupBy, GroupSortBy sortBy, SortOrder sortOrder, Boolean force) { - - if (!Case.isCaseOpen()) { - return; - } - - //only re-query the db if the data source or group by attribute changed or it is forced - if (dataSource != getDataSource() - || groupBy != getGroupBy() - || force) { - - setDataSource(dataSource); - setGroupBy(groupBy); - setSortBy(sortBy); - setSortOrder(sortOrder); - if (groupByTask != null) { - groupByTask.cancel(true); - } - - groupByTask = new ReGroupTask<>(dataSource, groupBy, sortBy, sortOrder); - Platform.runLater(() -> regroupProgress.bind(groupByTask.progressProperty())); - exec.submit(groupByTask); - } else { - // resort the list of groups - setSortBy(sortBy); - setSortOrder(sortOrder); - Platform.runLater(() -> { - FXCollections.sort(analyzedGroups, makeGroupComparator(sortOrder, sortBy)); - FXCollections.sort(unSeenGroups, makeGroupComparator(sortOrder, sortBy)); - }); - } - } - - public ReadOnlyDoubleProperty regroupProgress() { - return regroupProgress.getReadOnlyProperty(); - } - - @Subscribe - synchronized public void handleTagAdded(ContentTagAddedEvent evt) { - GroupKey newGroupKey = null; - final long fileID = evt.getAddedTag().getContent().getId(); - if (getGroupBy() == DrawableAttribute.CATEGORY && CategoryManager.isCategoryTagName(evt.getAddedTag().getName())) { - newGroupKey = new GroupKey<>(DrawableAttribute.CATEGORY, CategoryManager.categoryFromTagName(evt.getAddedTag().getName()), getDataSource()); - for (GroupKey oldGroupKey : groupMap.keySet()) { - if (oldGroupKey.equals(newGroupKey) == false) { - removeFromGroup(oldGroupKey, fileID); - } - } - } else if (getGroupBy() == DrawableAttribute.TAGS && CategoryManager.isNotCategoryTagName(evt.getAddedTag().getName())) { - newGroupKey = new GroupKey<>(DrawableAttribute.TAGS, evt.getAddedTag().getName(), getDataSource()); - } - if (newGroupKey != null) { - DrawableGroup g = getGroupForKey(newGroupKey); - addFileToGroup(g, newGroupKey, fileID); - } - } - - @SuppressWarnings("AssignmentToMethodParameter") - synchronized private void addFileToGroup(DrawableGroup g, final GroupKey groupKey, final long fileID) { - if (g == null) { - //if there wasn't already a group check if there should be one now - g = popuplateIfAnalyzed(groupKey, null); - } - DrawableGroup group = g; - if (group != null) { - //if there is aleady a group that was previously deemed fully analyzed, then add this newly analyzed file to it. - group.addFile(fileID); - } - } - - @Subscribe - synchronized public void handleTagDeleted(ContentTagDeletedEvent evt) { - GroupKey groupKey = null; - final ContentTagDeletedEvent.DeletedContentTagInfo deletedTagInfo = evt.getDeletedTagInfo(); - final TagName tagName = deletedTagInfo.getName(); - if (getGroupBy() == DrawableAttribute.CATEGORY && CategoryManager.isCategoryTagName(tagName)) { - groupKey = new GroupKey<>(DrawableAttribute.CATEGORY, CategoryManager.categoryFromTagName(tagName), getDataSource()); - } else if (getGroupBy() == DrawableAttribute.TAGS && CategoryManager.isNotCategoryTagName(tagName)) { - groupKey = new GroupKey<>(DrawableAttribute.TAGS, tagName, getDataSource()); - } - if (groupKey != null) { - final long fileID = deletedTagInfo.getContentID(); - DrawableGroup g = removeFromGroup(groupKey, fileID); - } - } - - @Subscribe - synchronized public void handleFileRemoved(Collection removedFileIDs) { - - for (final long fileId : removedFileIDs) { - //get grouping(s) this file would be in - Set> groupsForFile = getGroupKeysForFileID(fileId); - - for (GroupKey gk : groupsForFile) { - removeFromGroup(gk, fileId); - } - } - } - - /** - * Handle notifications sent from Db when files are inserted/updated - * - * @param updatedFileIDs The ID of the inserted/updated files. - */ - @Subscribe - synchronized public void handleFileUpdate(Collection updatedFileIDs) { - /** - * TODO: is there a way to optimize this to avoid quering to db so much. - * the problem is that as a new files are analyzed they might be in new - * groups( if we are grouping by say make or model) -jm - */ - for (long fileId : updatedFileIDs) { - - controller.getHashSetManager().invalidateHashSetsForFile(fileId); - - //get grouping(s) this file would be in - Set> groupsForFile = getGroupKeysForFileID(fileId); - for (GroupKey gk : groupsForFile) { - DrawableGroup g = getGroupForKey(gk); - addFileToGroup(g, gk, fileId); - } - } - - //we fire this event for all files so that the category counts get updated during initial db population - controller.getCategoryManager().fireChange(updatedFileIDs, null); - } - - synchronized private DrawableGroup popuplateIfAnalyzed(GroupKey groupKey, ReGroupTask task) { - /* - * If this method call is part of a ReGroupTask and that task is - * cancelled, no-op. - * - * This allows us to stop if a regroup task has been cancelled (e.g. the - * user picked a different group by attribute, while the current task - * was still running) - */ - if (isNull(task) || task.isCancelled() == false) { - DrawableDB db = getDB(); - /* - * For attributes other than path we can't be sure a group is fully - * analyzed because we don't know all the files that will be a part - * of that group. just show them no matter what. - */ - if (nonNull(db) && ((groupKey.getAttribute() != DrawableAttribute.PATH) || db.isGroupAnalyzed(groupKey))) { - try { - Set fileIDs = getFileIDsInGroup(groupKey); - if (Objects.nonNull(fileIDs)) { - DrawableGroup group; - final boolean groupSeen = db.isGroupSeen(groupKey); - if (groupMap.containsKey(groupKey)) { - group = groupMap.get(groupKey); - group.setFiles(ObjectUtils.defaultIfNull(fileIDs, Collections.emptySet())); - group.setSeen(groupSeen); - } else { - group = new DrawableGroup(groupKey, fileIDs, groupSeen); - controller.getCategoryManager().registerListener(group); - groupMap.put(groupKey, group); - } - - if (analyzedGroups.contains(group) == false) { - analyzedGroups.add(group); - if (isNull(task)) { - sortAnalyzedGroups(); - } - } - updateUnSeenGroups(group, groupSeen); - - return group; - - } - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "failed to get files for group: " + groupKey.getAttribute().attrName.toString() + " = " + groupKey.getValue(), ex); //NON-NLS - } - } - } - - return null; - } - - synchronized public Set getFileIDsWithMimeType(String mimeType) throws TskCoreException { - - HashSet hashSet = new HashSet<>(); - String query = (null == mimeType) - ? "SELECT obj_id FROM tsk_files WHERE mime_type IS NULL" //NON-NLS - : "SELECT obj_id FROM tsk_files WHERE mime_type = '" + mimeType + "'"; //NON-NLS - DrawableDB db = getDB(); - try (SleuthkitCase.CaseDbQuery executeQuery = controller.getSleuthKitCase().executeQuery(query); - ResultSet resultSet = executeQuery.getResultSet();) { - while (resultSet.next()) { - final long fileID = resultSet.getLong("obj_id"); //NON-NLS - if (nonNull(db) && db.isInDB(fileID)) { - hashSet.add(fileID); - } - } - return hashSet; - - } catch (Exception ex) { - throw new TskCoreException("Failed to get file ids with mime type " + mimeType, ex); - } - } - - /** - * Task to query database for files in sorted groups and build - * DrawableGroups for them. - */ - @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}"}) - private class ReGroupTask> extends LoggedTask { - - private final DataSource dataSource; - private final DrawableAttribute groupBy; - private final GroupSortBy sortBy; - private final SortOrder sortOrder; - - private final ProgressHandle groupProgress; - - ReGroupTask(DataSource dataSource, DrawableAttribute groupBy, GroupSortBy sortBy, SortOrder sortOrder) { - super(Bundle.ReGroupTask_displayTitle(groupBy.attrName.toString(), sortBy.getDisplayName(), sortOrder.toString()), true); - this.dataSource = dataSource; - this.groupBy = groupBy; - this.sortBy = sortBy; - this.sortOrder = sortOrder; - - groupProgress = ProgressHandle.createHandle(Bundle.ReGroupTask_displayTitle(groupBy.attrName.toString(), sortBy.getDisplayName(), sortOrder.toString()), this); - } - - @Override - public boolean isCancelled() { - return super.isCancelled(); - } - - @Override - protected Void call() throws Exception { - - if (isCancelled()) { - return null; - } - groupProgress.start(); - - synchronized (GroupManager.this) { - analyzedGroups.clear(); - unSeenGroups.clear(); - - // Get the list of group keys - final Multimap valsByDataSource = findValuesForAttribute(); - groupProgress.switchToDeterminate(valsByDataSource.size()); - int p = 0; - // For each key value, partially create the group and add it to the list. - for (final Map.Entry val : valsByDataSource.entries()) { - if (isCancelled()) { - return null; - } - p++; - updateMessage(Bundle.ReGroupTask_progressUpdate(groupBy.attrName.toString(), val.getValue())); - updateProgress(p, valsByDataSource.size()); - groupProgress.progress(Bundle.ReGroupTask_progressUpdate(groupBy.attrName.toString(), val), p); - popuplateIfAnalyzed(new GroupKey<>(groupBy, val.getValue(), val.getKey()), this); - } - } - - DataSource dataSourceOfCurrentGroup - = Optional.ofNullable(controller.getViewState()) - .flatMap(GroupViewState::getGroup) - .map(DrawableGroup::getGroupKey) - .flatMap(GroupKey::getDataSource) - .orElse(null); - if (getDataSource() == null - || Objects.equals(dataSourceOfCurrentGroup, getDataSource())) { - //the current group is for the given datasource, so just keep it in view. - } else { //the current group should not be visible so ... - if (isNotEmpty(unSeenGroups)) {// show then next unseen group - controller.advance(GroupViewState.tile(unSeenGroups.get(0)), false); - } else { // clear the group area. - controller.advance(GroupViewState.tile(null), false); - } - } - - groupProgress.finish(); - updateProgress(1, 1); - return null; - } - - @Override - protected void done() { - super.done(); - try { - get(); - } catch (CancellationException cancelEx) { - //cancellation is normal - } catch (InterruptedException | ExecutionException ex) { - logger.log(Level.SEVERE, "Error while regrouping.", ex); - } - } - - /** - * find the distinct values for the given column (DrawableAttribute) - * - * These values represent the groups of files. - * - * @param groupBy - * - * @return - */ - public Multimap findValuesForAttribute() { - synchronized (GroupManager.this) { - DrawableDB db = getDB(); - Multimap results = HashMultimap.create(); - try { - switch (groupBy.attrName) { - //these cases get special treatment - case CATEGORY: - results.putAll(null, Arrays.asList(DhsImageCategory.values())); - break; - case TAGS: - results.putAll(null, controller.getTagsManager().getTagNamesInUse().stream() - .filter(CategoryManager::isNotCategoryTagName) - .collect(Collectors.toList())); - break; - - case ANALYZED: - results.putAll(null, Arrays.asList(false, true)); - break; - case HASHSET: - if (nonNull(db)) { - results.putAll(null, new TreeSet<>(db.getHashSetNames())); - } - break; - case MIME_TYPE: - if (nonNull(db)) { - HashSet types = new HashSet<>(); - - // Use the group_concat function to get a list of files for each mime type. - // This has different syntax on Postgres vs SQLite - String groupConcatClause; - if (DbType.POSTGRESQL == controller.getSleuthKitCase().getDatabaseType()) { - groupConcatClause = " array_to_string(array_agg(obj_id), ',') as object_ids"; - } else { - groupConcatClause = " group_concat(obj_id) as object_ids"; - } - String query = "select " + groupConcatClause + " , mime_type from tsk_files group by mime_type "; - try (SleuthkitCase.CaseDbQuery executeQuery = controller.getSleuthKitCase().executeQuery(query); //NON-NLS - ResultSet resultSet = executeQuery.getResultSet();) { - while (resultSet.next()) { - final String mimeType = resultSet.getString("mime_type"); //NON-NLS - String objIds = resultSet.getString("object_ids"); //NON-NLS - - Pattern.compile(",").splitAsStream(objIds) - .map(Long::valueOf) - .filter(db::isInDB) - .findAny().ifPresent(obj_id -> types.add(mimeType)); - } - } catch (SQLException | TskCoreException ex) { - Exceptions.printStackTrace(ex); - } - results.putAll(null, types); - } - break; - default: - //otherwise do straight db query - if (nonNull(db)) { - results.putAll(db.findValuesForAttribute(groupBy, sortBy, sortOrder, dataSource)); - } - } - - } catch (TskCoreException | TskDataException ex) { - logger.log(Level.SEVERE, "TSK error getting list of type {0}", groupBy.getDisplayName()); //NON-NLS - } - return results; - } - } - } - - private static Comparator makeGroupComparator(final SortOrder sortOrder, GroupSortBy 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/gui/drawableviews/DrawableUIBase.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableUIBase.java index dd26ecd4c7..6f0b1d1c2a 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableUIBase.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableUIBase.java @@ -40,7 +40,6 @@ import javafx.scene.layout.BorderPane; import javafx.scene.layout.VBox; import org.controlsfx.control.action.ActionUtils; import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.actions.OpenExternalViewerAction; @@ -54,10 +53,10 @@ import org.sleuthkit.datamodel.TskCoreException; "DrawableUIBase.errorLabel.OOMText=Insufficent memory"}) abstract public class DrawableUIBase extends AnchorPane implements DrawableView { + /** The use of SingleThreadExecutor means we can only load a single image at + * a time */ static final Executor exec = Executors.newSingleThreadExecutor(); - private static final Logger logger = Logger.getLogger(DrawableUIBase.class.getName()); - @FXML BorderPane imageBorder; @FXML @@ -132,13 +131,13 @@ abstract public class DrawableUIBase extends AnchorPane implements DrawableView Platform.runLater(() -> imageBorder.setCenter(progressNode)); //called on fx thread - myTask.setOnSucceeded(succeeded -> { + myTask.setOnSucceeded(succeeded -> { //on fx thread showImage(file, myTask); synchronized (DrawableUIBase.this) { imageTask = null; } }); - myTask.setOnFailed(failed -> { + myTask.setOnFailed(failed -> { //on fx thread Throwable exception = myTask.getException(); if (exception instanceof OutOfMemoryError && exception.getMessage().contains("Java heap space")) { //NON-NLS @@ -150,7 +149,7 @@ abstract public class DrawableUIBase extends AnchorPane implements DrawableView imageTask = null; } }); - myTask.setOnCancelled(cancelled -> { + myTask.setOnCancelled(cancelled -> { //on fx thread synchronized (DrawableUIBase.this) { imageTask = null; } @@ -174,9 +173,12 @@ abstract public class DrawableUIBase extends AnchorPane implements DrawableView } /** + * Get a new progress indicator to use as a place holder for the image in + * this view. * - * @param file the value of file - * @param imageTask the value of imageTask + * @param imageTask The imageTask to get a progress indicator for. + * + * @return The new Node to use as a progress indicator. */ Node newProgressIndicator(final Task imageTask) { ProgressIndicator loadingProgressIndicator = new ProgressIndicator(-1); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.fxml b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.fxml index ec6e539f87..0e788327c4 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.fxml +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.fxml @@ -2,6 +2,7 @@ + @@ -11,6 +12,7 @@ + @@ -18,7 +20,7 @@ - +
@@ -29,7 +31,7 @@ diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java index d730f8346a..12fe8b576d 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java @@ -57,7 +57,9 @@ import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.geometry.Bounds; +import javafx.scene.Cursor; import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.MenuItem; @@ -85,6 +87,7 @@ import static javafx.scene.input.KeyCode.RIGHT; import static javafx.scene.input.KeyCode.UP; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; +import javafx.scene.layout.AnchorPane; import javafx.scene.layout.Border; import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderStroke; @@ -95,6 +98,7 @@ import javafx.scene.layout.HBox; import javafx.scene.paint.Color; import javafx.util.Duration; import javax.swing.SwingUtilities; +import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; import org.apache.commons.lang3.StringUtils; import org.controlsfx.control.GridCell; import org.controlsfx.control.GridView; @@ -135,9 +139,8 @@ import org.sleuthkit.autopsy.imagegallery.utils.TaskUtils; import org.sleuthkit.datamodel.TskCoreException; /** - * A GroupPane displays the contents of a {@link DrawableGroup}. It supports - * both a {@link GridView} based view and a {@link SlideShowView} view by - * swapping out its internal components. + * A GroupPane displays the contents of a DrawableGroup. It supports both + * GridView and SlideShowView modes by swapping out its internal components. * * * TODO: Extract the The GridView instance to a separate class analogous to the @@ -150,26 +153,20 @@ import org.sleuthkit.datamodel.TskCoreException; public class GroupPane extends BorderPane { private static final Logger logger = Logger.getLogger(GroupPane.class.getName()); - private final ListeningExecutorService exec = TaskUtils.getExecutorForClass(GroupPane.class); private static final BorderWidths BORDER_WIDTHS_2 = new BorderWidths(2); private static final CornerRadii CORNER_RADII_2 = new CornerRadii(2); private static final DropShadow DROP_SHADOW = new DropShadow(10, Color.BLUE); - private static final Timeline flashAnimation = new Timeline(new KeyFrame(Duration.millis(400), new KeyValue(DROP_SHADOW.radiusProperty(), 1, Interpolator.LINEAR)), + private static final Timeline flashAnimation = new Timeline( + new KeyFrame(Duration.millis(400), new KeyValue(DROP_SHADOW.radiusProperty(), 1, Interpolator.LINEAR)), new KeyFrame(Duration.millis(400), new KeyValue(DROP_SHADOW.radiusProperty(), 15, Interpolator.LINEAR)) ); - private final FileIDSelectionModel selectionModel; - - private static final List categoryKeyCodes - = Arrays.asList(KeyCode.NUMPAD0, KeyCode.NUMPAD1, KeyCode.NUMPAD2, KeyCode.NUMPAD3, KeyCode.NUMPAD4, KeyCode.NUMPAD5, - KeyCode.DIGIT0, KeyCode.DIGIT1, KeyCode.DIGIT2, KeyCode.DIGIT3, KeyCode.DIGIT4, KeyCode.DIGIT5); - - private final Back backAction; - - private final Forward forwardAction; + private static final List categoryKeyCodes = Arrays.asList( + NUMPAD0, NUMPAD1, NUMPAD2, NUMPAD3, NUMPAD4, NUMPAD5, + DIGIT0, DIGIT1, DIGIT2, DIGIT3, DIGIT4, DIGIT5); @FXML private Button undoButton; @@ -178,13 +175,10 @@ public class GroupPane extends BorderPane { @FXML private SplitMenuButton catSelectedSplitMenu; - @FXML private SplitMenuButton tagSelectedSplitMenu; - @FXML private ToolBar headerToolBar; - @FXML private ToggleButton cat0Toggle; @FXML @@ -201,26 +195,25 @@ public class GroupPane extends BorderPane { @FXML private SegmentedButton segButton; - private SlideShowView slideShowPane; - @FXML private ToggleButton slideShowToggle; - - @FXML - private GridView gridView; - @FXML private ToggleButton tileToggle; + private SlideShowView slideShowPane; + + @FXML + private GridView gridView; @FXML private Button nextButton; - + @FXML + private AnchorPane nextButtonPane; + @FXML + private CheckBox seenByOtherExaminersCheckBox; @FXML private Button backButton; - @FXML private Button forwardButton; - @FXML private Label groupLabel; @FXML @@ -237,30 +230,27 @@ public class GroupPane extends BorderPane { @FXML private HBox catSplitMenuContainer; - private final KeyboardHandler tileKeyboardNavigationHandler = new KeyboardHandler(); - - private final NextUnseenGroup nextGroupAction; + private final ListeningExecutorService exec = TaskUtils.getExecutorForClass(GroupPane.class); private final ImageGalleryController controller; - private ContextMenu contextMenu; - + private final FileIDSelectionModel selectionModel; private Integer selectionAnchorIndex; + private final UndoAction undoAction; private final RedoAction redoAction; + private final Back backAction; + private final Forward forwardAction; + private final NextUnseenGroup nextGroupAction; - GroupViewMode getGroupViewMode() { - return groupViewMode.get(); - } + private final KeyboardHandler tileKeyboardNavigationHandler = new KeyboardHandler(); - /** - * the current GroupViewMode of this GroupPane - */ + private ContextMenu contextMenu; + + /** the current GroupViewMode of this GroupPane */ private final SimpleObjectProperty groupViewMode = new SimpleObjectProperty<>(GroupViewMode.TILE); - /** - * the grouping this pane is currently the view for - */ + /** the grouping this pane is currently the view for */ private final ReadOnlyObjectWrapper grouping = new ReadOnlyObjectWrapper<>(); /** @@ -294,6 +284,10 @@ public class GroupPane extends BorderPane { FXMLConstructor.construct(this, "GroupPane.fxml"); //NON-NLS } + GroupViewMode getGroupViewMode() { + return groupViewMode.get(); + } + @ThreadConfined(type = ThreadType.JFX) public void activateSlideShowViewer(Long slideShowFileID) { groupViewMode.set(GroupViewMode.SLIDE_SHOW); @@ -340,7 +334,9 @@ public class GroupPane extends BorderPane { } /** - * create the string to display in the group header + * Create the string to display in the group header. + * + * @return The string to display in the group header. */ @NbBundle.Messages({"# {0} - default group name", "# {1} - hashset hits count", @@ -391,19 +387,20 @@ public class GroupPane extends BorderPane { "GroupPane.catContainerLabel.displayText=Categorize Selected File:", "GroupPane.catHeadingLabel.displayText=Category:"}) void initialize() { - assert cat0Toggle != null : "fx:id=\"cat0Toggle\" was not injected: check your FXML file 'SlideShowView.fxml'."; - assert cat1Toggle != null : "fx:id=\"cat1Toggle\" was not injected: check your FXML file 'SlideShowView.fxml'."; - assert cat2Toggle != null : "fx:id=\"cat2Toggle\" was not injected: check your FXML file 'SlideShowView.fxml'."; - assert cat3Toggle != null : "fx:id=\"cat3Toggle\" was not injected: check your FXML file 'SlideShowView.fxml'."; - assert cat4Toggle != null : "fx:id=\"cat4Toggle\" was not injected: check your FXML file 'SlideShowView.fxml'."; - assert cat5Toggle != null : "fx:id=\"cat5Toggle\" was not injected: check your FXML file 'SlideShowView.fxml'."; + assert cat0Toggle != null : "fx:id=\"cat0Toggle\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert cat1Toggle != null : "fx:id=\"cat1Toggle\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert cat2Toggle != null : "fx:id=\"cat2Toggle\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert cat3Toggle != null : "fx:id=\"cat3Toggle\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert cat4Toggle != null : "fx:id=\"cat4Toggle\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert cat5Toggle != null : "fx:id=\"cat5Toggle\" was not injected: check your FXML file 'GroupPane.fxml'."; assert gridView != null : "fx:id=\"tilePane\" was not injected: check your FXML file 'GroupPane.fxml'."; - assert catSelectedSplitMenu != null : "fx:id=\"grpCatSplitMenu\" was not injected: check your FXML file 'GroupHeader.fxml'."; - assert tagSelectedSplitMenu != null : "fx:id=\"grpTagSplitMenu\" was not injected: check your FXML file 'GroupHeader.fxml'."; - assert headerToolBar != null : "fx:id=\"headerToolBar\" was not injected: check your FXML file 'GroupHeader.fxml'."; - assert segButton != null : "fx:id=\"previewList\" was not injected: check your FXML file 'GroupHeader.fxml'."; - assert slideShowToggle != null : "fx:id=\"segButton\" was not injected: check your FXML file 'GroupHeader.fxml'."; - assert tileToggle != null : "fx:id=\"tileToggle\" was not injected: check your FXML file 'GroupHeader.fxml'."; + assert catSelectedSplitMenu != null : "fx:id=\"grpCatSplitMenu\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert tagSelectedSplitMenu != null : "fx:id=\"grpTagSplitMenu\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert headerToolBar != null : "fx:id=\"headerToolBar\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert segButton != null : "fx:id=\"previewList\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert slideShowToggle != null : "fx:id=\"segButton\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert tileToggle != null : "fx:id=\"tileToggle\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert seenByOtherExaminersCheckBox != null : "fx:id=\"seenByOtherExaminersCheckBox\" was not injected: check your FXML file 'GroupPane.fxml'."; for (DhsImageCategory cat : DhsImageCategory.values()) { ToggleButton toggleForCategory = getToggleForCategory(cat); @@ -530,6 +527,16 @@ public class GroupPane extends BorderPane { } }); + seenByOtherExaminersCheckBox.selectedProperty().addListener((observable, oldValue, newValue) -> { + nextButtonPane.setDisable(true); + nextButtonPane.setCursor(Cursor.WAIT); + exec.submit(() -> controller.getGroupManager().setCollaborativeMode(newValue)) + .addListener(() -> { + nextButtonPane.setDisable(false); + nextButtonPane.setCursor(Cursor.DEFAULT); + }, Platform::runLater); + }); + //listen to tile selection and make sure it is visible in scroll area selectionModel.lastSelectedProperty().addListener((observable, oldFileID, newFileId) -> { if (groupViewMode.get() == GroupViewMode.SLIDE_SHOW @@ -607,7 +614,7 @@ public class GroupPane extends BorderPane { * assigns a grouping for this pane to represent and initializes grouping * specific properties and listeners * - * @param grouping the new grouping assigned to this group + * @param newViewState */ void setViewState(GroupViewState newViewState) { @@ -894,7 +901,7 @@ public class GroupPane extends BorderPane { if (t.getClickCount() == 1) { selectAllFiles(); } - if (selectionModel.getSelected().isEmpty() == false) { + if (isNotEmpty(selectionModel.getSelected())) { if (contextMenu == null) { contextMenu = buildContextMenu(); } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupCellFactory.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupCellFactory.java index d21b76d018..72f602d06a 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupCellFactory.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupCellFactory.java @@ -173,7 +173,7 @@ class GroupCellFactory { private final InvalidationListener groupListener = new GroupListener<>(this); /** - * reference to group files listener that allows us to remove it from a + * Reference to group files listener that allows us to remove it from a * group when a new group is assigned to this Cell */ @Override