Merge pull request #4100 from millmanorama/1012-examiner_seen_UI_integration

1012 examiner seen ui integration
This commit is contained in:
Richard Cordovano 2018-09-17 09:06:41 -04:00 committed by GitHub
commit eb2193dd6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 219 additions and 1085 deletions

View File

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

View File

@ -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<DrawableGroup> 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) {
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 {
//there are more unseen groups.
Platform.runLater(() -> {
setDisabled(false);
setText(NEXT_UNSEEN_GROUP);
setGraphic(new ImageView(ADVANCE_IMAGE));
}
});
}
}
}
}

View File

@ -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<Boolean> 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
}
}
}
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();
try {
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

View File

@ -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<DrawableGroup> {
return seen.get();
}
public ReadOnlyBooleanWrapper seenProperty() {
return seen;
public ReadOnlyBooleanProperty seenProperty() {
return seen.getReadOnlyProperty();
}
@Subscribe

View File

@ -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<SortOrder> sortOrderProp = new ReadOnlyObjectWrapper<>(SortOrder.ASCENDING);
private final ReadOnlyObjectWrapper<DataSource> 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<Long> 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 <AttrValType> The type of the values that this task will group by.
*/
@SuppressWarnings({"unchecked", "rawtypes"})
@NbBundle.Messages({"# {0} - groupBy attribute Name",

View File

@ -1,888 +0,0 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2013-18 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.imagegallery.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<DrawableGroup> analyzedGroups = FXCollections.observableArrayList();
private final ObservableList<DrawableGroup> unmodifiableAnalyzedGroups = FXCollections.unmodifiableObservableList(analyzedGroups);
/** list of unseen groups */
@GuardedBy("this")
private final ObservableList<DrawableGroup> unSeenGroups = FXCollections.observableArrayList();
private final ObservableList<DrawableGroup> 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<GroupKey<?>, 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<SortOrder> sortOrderProp = new ReadOnlyObjectWrapper<>(SortOrder.ASCENDING);
private final ReadOnlyObjectWrapper<DataSource> 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<DrawableGroup> getAnalyzedGroups() {
return unmodifiableAnalyzedGroups;
}
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public ObservableList<DrawableGroup> 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<GroupKey<?>> getGroupKeysForFile(DrawableFile file) {
Set<GroupKey<?>> 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<GroupKey<?>> 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<Long> getFileIDsInGroup(GroupKey<?> groupKey) throws TskCoreException {
Set<Long> 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<Long> getFileIDsWithCategory(DhsImageCategory category) throws TskCoreException {
Set<Long> 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<Long> files = new HashSet<>();
for (TagName tn : tns) {
if (tn != null) {
List<ContentTag> 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<ContentTag> 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<Long> getFileIDsWithTag(TagName tagName) throws TskCoreException {
Set<Long> files = new HashSet<>();
try {
List<ContentTag> 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<DrawableAttribute<?>> getGroupByProperty() {
return groupByProp.getReadOnlyProperty();
}
public synchronized SortOrder getSortOrder() {
return sortOrderProp.get();
}
synchronized void setSortOrder(SortOrder sortOrder) {
sortOrderProp.set(sortOrder);
}
public ReadOnlyObjectProperty<SortOrder> getSortOrderProperty() {
return sortOrderProp.getReadOnlyProperty();
}
public synchronized DataSource getDataSource() {
return dataSourceProp.get();
}
synchronized void setDataSource(DataSource dataSource) {
dataSourceProp.set(dataSource);
}
public ReadOnlyObjectProperty<DataSource> getDataSourceProperty() {
return dataSourceProp.getReadOnlyProperty();
}
/**
* Regroup all files in the database. see ReGroupTask for more details.
*
* @param <A> 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 <A extends Comparable<A>> void regroup(DataSource dataSource, DrawableAttribute<A> 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<Long> removedFileIDs) {
for (final long fileId : removedFileIDs) {
//get grouping(s) this file would be in
Set<GroupKey<?>> 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<Long> 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<GroupKey<?>> 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<Long> 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<Long> getFileIDsWithMimeType(String mimeType) throws TskCoreException {
HashSet<Long> 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<AttrValType extends Comparable<AttrValType>> extends LoggedTask<Void> {
private final DataSource dataSource;
private final DrawableAttribute<AttrValType> groupBy;
private final GroupSortBy sortBy;
private final SortOrder sortOrder;
private final ProgressHandle groupProgress;
ReGroupTask(DataSource dataSource, DrawableAttribute<AttrValType> 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<DataSource, AttrValType> 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<DataSource, AttrValType> 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<DataSource, AttrValType> 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<String> 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<DrawableGroup> makeGroupComparator(final SortOrder sortOrder, GroupSortBy comparator) {
switch (sortOrder) {
case ASCENDING:
return comparator;
case DESCENDING:
return comparator.reversed();
case UNSORTED:
default:
return new GroupSortBy.AllEqualComparator<>();
}
}
}

View File

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

View File

@ -2,6 +2,7 @@
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.RadioButton?>
@ -11,6 +12,7 @@
<?import javafx.scene.control.ToolBar?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
@ -18,7 +20,7 @@
<?import org.controlsfx.control.GridView?>
<?import org.controlsfx.control.SegmentedButton?>
<fx:root type="BorderPane" xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1">
<fx:root type="BorderPane" xmlns="http://javafx.com/javafx/8.0.141" xmlns:fx="http://javafx.com/fxml/1">
<center>
<GridView fx:id="gridView" BorderPane.alignment="CENTER" />
@ -29,7 +31,7 @@
<HBox alignment="CENTER_LEFT" spacing="5.0" BorderPane.alignment="TOP_LEFT">
<children>
<Label fx:id="bottomLabel" text="Group Viewing History: " />
<Button fx:id="backButton" mnemonicParsing="false" text="back">
<Button fx:id="backButton" mnemonicParsing="false" text="Back">
<graphic>
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
<image>
@ -38,7 +40,7 @@
</ImageView>
</graphic>
</Button>
<Button fx:id="forwardButton" mnemonicParsing="false" text="forward">
<Button fx:id="forwardButton" mnemonicParsing="false" text="Forward">
<graphic>
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
<image>
@ -56,10 +58,13 @@
<right>
<HBox alignment="CENTER_RIGHT" spacing="5.0" BorderPane.alignment="TOP_RIGHT">
<children>
<Button fx:id="nextButton" contentDisplay="RIGHT" mnemonicParsing="false" text="next unseen group" BorderPane.alignment="CENTER_RIGHT">
<CheckBox fx:id="seenByOtherExaminersCheckBox" mnemonicParsing="false" text="Don't show groups seen by other examiners" />
<AnchorPane fx:id="nextButtonPane" BorderPane.alignment="CENTER_RIGHT">
<BorderPane.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</BorderPane.margin>
<children>
<Button fx:id="nextButton" contentDisplay="RIGHT" minWidth="175.0" mnemonicParsing="false" text="All Groups Gave Been Seen">
<graphic>
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
<image>
@ -69,6 +74,8 @@
</graphic>
</Button>
</children>
</AnchorPane>
</children>
<BorderPane.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</BorderPane.margin>

View File

@ -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<KeyCode> 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<KeyCode> 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<Long> gridView;
@FXML
private ToggleButton tileToggle;
private SlideShowView slideShowPane;
@FXML
private GridView<Long> 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> 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<DrawableGroup> 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();
}

View File

@ -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