mirror of
https://github.com/overcuriousity/autopsy-flatpak.git
synced 2025-07-11 23:46:15 +00:00
Merge pull request #4100 from millmanorama/1012-examiner_seen_UI_integration
1012 examiner seen ui integration
This commit is contained in:
commit
eb2193dd6f
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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) {
|
||||
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));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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<>();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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,18 +58,23 @@
|
||||
<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">
|
||||
<BorderPane.margin>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</BorderPane.margin>
|
||||
<graphic>
|
||||
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true">
|
||||
<image>
|
||||
<Image url="@../../images/control-double.png" />
|
||||
</image>
|
||||
</ImageView>
|
||||
</graphic>
|
||||
</Button>
|
||||
<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>
|
||||
<Image url="@../../images/control-double.png" />
|
||||
</image>
|
||||
</ImageView>
|
||||
</graphic>
|
||||
</Button>
|
||||
</children>
|
||||
</AnchorPane>
|
||||
</children>
|
||||
<BorderPane.margin>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user