From df567df149a9aeb844924cd6fb95fa47b0d3d7e7 Mon Sep 17 00:00:00 2001 From: millmanorama Date: Sat, 15 Sep 2018 12:46:01 +0200 Subject: [PATCH 1/4] cleanup --- .../imagegallery/actions/NextUnseenGroup.java | 2 +- .../imagegallery/datamodel/DrawableDB.java | 89 +++++++++---------- .../datamodel/grouping/GroupManager.java | 4 +- 3 files changed, 42 insertions(+), 53 deletions(-) diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/NextUnseenGroup.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/NextUnseenGroup.java index 7cae13caa3..22f0fcb12b 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/NextUnseenGroup.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/NextUnseenGroup.java @@ -69,7 +69,7 @@ public class NextUnseenGroup extends Action { Optional.ofNullable(controller.getViewState()) .flatMap(GroupViewState::getGroup) .ifPresent(group -> { - groupManager.setGroupSeen(group, true) + groupManager.markGroupSeen(group, true) .addListener(this::advanceToNextUnseenGroup, MoreExecutors.newDirectExecutorService()); }); }); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java index df19e57aa1..0558e895a5 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java @@ -42,6 +42,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Function; import java.util.logging.Level; import java.util.stream.Collectors; import javax.annotation.Nonnull; @@ -629,6 +630,41 @@ public final class DrawableDB { return names; } + // Callback to process result of seen query + private static class GroupSeenQueryResultProcessor implements CaseDbAccessQueryCallback { + + private interface SQLFunction { + + T2 apply(T1 rs) throws SQLException; + } + + private final SQLFunction resultExtractor; + + GroupSeenQueryResultProcessor(SQLFunction resultExtractor) { + this.resultExtractor = resultExtractor; + } + + private boolean seen = false; + + boolean getGroupSeen() { + return seen; + } + + @Override + public void process(ResultSet resultSet) { + try { + if (resultSet != null) { + while (resultSet.next()) { + seen = resultExtractor.apply(resultSet); //NON-NLS; + return; + } + } + } catch (SQLException ex) { + logger.log(Level.SEVERE, "failed to get group seen", ex); //NON-NLS + } + } + } + /** * Returns true if the specified group has been any examiner * @@ -637,30 +673,9 @@ public final class DrawableDB { * @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()) { - seen = resultSet.getInt("count") > 0; - return; - } - } - } catch (SQLException ex) { - logger.log(Level.SEVERE, "failed to get group seen", ex); //NON-NLS - } - } - } + GroupSeenQueryResultProcessor queryResultProcessor + = new GroupSeenQueryResultProcessor(rs -> rs.getInt("count") > 0); try { @@ -674,8 +689,6 @@ public final class DrawableDB { String groupSeenQueryStmt = "COUNT((*) as count FROM " + GROUPS_SEEN_TABLENAME + " WHERE seen = 1 AND group_id in ( " + groupIdQuery + ")"; - GroupSeenQueryResultProcessor queryResultProcessor = new GroupSeenQueryResultProcessor(); - tskCase.getCaseDbAccessManager().select(groupSeenQueryStmt, queryResultProcessor); return queryResultProcessor.getGroupSeen(); } catch (TskCoreException ex) { @@ -696,38 +709,16 @@ public final class DrawableDB { * @return true if the examine has this group, false otherwise */ public boolean isGroupSeenByExaminer(GroupKey groupKey, long examinerId) { - // Callback to process result of seen query - class GroupSeenQueryResultProcessor implements CaseDbAccessQueryCallback { + GroupSeenQueryResultProcessor queryResultProcessor + = new GroupSeenQueryResultProcessor(rs -> rs.getBoolean("seen")); - private boolean seen = false; - - boolean getGroupSeen() { - return seen; - } - - @Override - public void process(ResultSet resultSet) { - try { - if (resultSet != null) { - while (resultSet.next()) { - seen = resultSet.getBoolean("seen"); //NON-NLS; - 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 = 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(); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java index 3c7a6b99fe..04dad074e7 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java @@ -47,7 +47,6 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; 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; @@ -60,7 +59,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; @@ -100,7 +98,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; From 17bef5c68f54d87570def77b073525e4bdbd59ef Mon Sep 17 00:00:00 2001 From: millmanorama Date: Sun, 16 Sep 2018 12:24:24 +0200 Subject: [PATCH 2/4] wire seen by other examiners check box --- .../imagegallery/datamodel/DrawableDB.java | 7 +- .../datamodel/grouping/DrawableGroup.java | 5 +- .../datamodel/grouping/GroupManager.java | 39 ++++-- .../gui/drawableviews/GroupPane.fxml | 37 +++--- .../gui/drawableviews/GroupPane.java | 111 ++++++++++-------- .../gui/navpanel/GroupCellFactory.java | 2 +- 6 files changed, 121 insertions(+), 80 deletions(-) diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java index 3fa6b8a2b4..52d2f8d240 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java @@ -634,8 +634,8 @@ public final class DrawableDB { 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)", + 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); @@ -685,7 +685,8 @@ public final class DrawableDB { try { String groupSeenQueryStmt = "COUNT(*) as count FROM " + GROUPS_SEEN_TABLENAME - + " WHERE group_id in ( " + getGroupIdQuery(groupKey) + ")" + + " 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); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/DrawableGroup.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/DrawableGroup.java index 64a73d02b8..224ac378c6 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/DrawableGroup.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/DrawableGroup.java @@ -26,6 +26,7 @@ import java.util.logging.Level; import javafx.beans.binding.Bindings; import javafx.beans.binding.DoubleBinding; import javafx.beans.binding.IntegerBinding; +import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.ReadOnlyLongProperty; import javafx.beans.property.ReadOnlyLongWrapper; @@ -165,8 +166,8 @@ public class DrawableGroup implements Comparable { return seen.get(); } - public ReadOnlyBooleanWrapper seenProperty() { - return seen; + public ReadOnlyBooleanProperty seenProperty() { + return seen.getReadOnlyProperty(); } @Subscribe diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java index e48d4990cc..fa40fdb66a 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java @@ -42,10 +42,12 @@ import java.util.TreeSet; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; +import java.util.function.Consumer; 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; @@ -126,6 +128,7 @@ public class GroupManager { private final ReadOnlyObjectWrapper< DrawableAttribute> groupByProp = new ReadOnlyObjectWrapper<>(DrawableAttribute.PATH); private final ReadOnlyObjectWrapper sortOrderProp = new ReadOnlyObjectWrapper<>(SortOrder.ASCENDING); private final ReadOnlyObjectWrapper dataSourceProp = new ReadOnlyObjectWrapper<>(null);//null indicates all datasources + private final ReadOnlyBooleanWrapper collaborativeModeProp = new ReadOnlyBooleanWrapper(false); private final GroupingService regrouper; @@ -249,15 +252,15 @@ public class GroupManager { 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); @@ -580,8 +583,8 @@ public class GroupManager { Set fileIDs = getFileIDsInGroup(groupKey); if (Objects.nonNull(fileIDs)) { - Examiner examiner = controller.getSleuthKitCase().getCurrentExaminer(); - final boolean groupSeen = getDrawableDB().isGroupSeenByExaminer(groupKey, examiner.getId()); + long examinerID = collaborativeModeProp.get() ? -1 : controller.getSleuthKitCase().getCurrentExaminer().getId(); + final boolean groupSeen = getDrawableDB().isGroupSeenByExaminer(groupKey, examinerID); DrawableGroup group; if (groupMap.containsKey(groupKey)) { @@ -598,10 +601,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,6 +636,29 @@ 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. diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.fxml b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.fxml index ec6e539f87..fc286f3c7b 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.fxml +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.fxml @@ -2,6 +2,7 @@ + @@ -11,6 +12,7 @@ + @@ -18,7 +20,7 @@ - +
@@ -29,7 +31,7 @@ diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java index d730f8346a..faec0b768d 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java @@ -57,7 +57,9 @@ import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.geometry.Bounds; +import javafx.scene.Cursor; import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.MenuItem; @@ -85,6 +87,7 @@ import static javafx.scene.input.KeyCode.RIGHT; import static javafx.scene.input.KeyCode.UP; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; +import javafx.scene.layout.AnchorPane; import javafx.scene.layout.Border; import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderStroke; @@ -95,6 +98,7 @@ import javafx.scene.layout.HBox; import javafx.scene.paint.Color; import javafx.util.Duration; import javax.swing.SwingUtilities; +import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; import org.apache.commons.lang3.StringUtils; import org.controlsfx.control.GridCell; import org.controlsfx.control.GridView; @@ -135,9 +139,8 @@ import org.sleuthkit.autopsy.imagegallery.utils.TaskUtils; import org.sleuthkit.datamodel.TskCoreException; /** - * A GroupPane displays the contents of a {@link DrawableGroup}. It supports - * both a {@link GridView} based view and a {@link SlideShowView} view by - * swapping out its internal components. + * A GroupPane displays the contents of a DrawableGroup. It supports both + * GridView and SlideShowView modes by swapping out its internal components. * * * TODO: Extract the The GridView instance to a separate class analogous to the @@ -150,27 +153,21 @@ import org.sleuthkit.datamodel.TskCoreException; public class GroupPane extends BorderPane { private static final Logger logger = Logger.getLogger(GroupPane.class.getName()); - private final ListeningExecutorService exec = TaskUtils.getExecutorForClass(GroupPane.class); private static final BorderWidths BORDER_WIDTHS_2 = new BorderWidths(2); private static final CornerRadii CORNER_RADII_2 = new CornerRadii(2); private static final DropShadow DROP_SHADOW = new DropShadow(10, Color.BLUE); - private static final Timeline flashAnimation = new Timeline(new KeyFrame(Duration.millis(400), new KeyValue(DROP_SHADOW.radiusProperty(), 1, Interpolator.LINEAR)), + private static final Timeline flashAnimation = new Timeline( + new KeyFrame(Duration.millis(400), new KeyValue(DROP_SHADOW.radiusProperty(), 1, Interpolator.LINEAR)), new KeyFrame(Duration.millis(400), new KeyValue(DROP_SHADOW.radiusProperty(), 15, Interpolator.LINEAR)) ); - private final FileIDSelectionModel selectionModel; - private static final List categoryKeyCodes = Arrays.asList(KeyCode.NUMPAD0, KeyCode.NUMPAD1, KeyCode.NUMPAD2, KeyCode.NUMPAD3, KeyCode.NUMPAD4, KeyCode.NUMPAD5, KeyCode.DIGIT0, KeyCode.DIGIT1, KeyCode.DIGIT2, KeyCode.DIGIT3, KeyCode.DIGIT4, KeyCode.DIGIT5); - private final Back backAction; - - private final Forward forwardAction; - @FXML private Button undoButton; @FXML @@ -178,13 +175,10 @@ public class GroupPane extends BorderPane { @FXML private SplitMenuButton catSelectedSplitMenu; - @FXML private SplitMenuButton tagSelectedSplitMenu; - @FXML private ToolBar headerToolBar; - @FXML private ToggleButton cat0Toggle; @FXML @@ -201,26 +195,25 @@ public class GroupPane extends BorderPane { @FXML private SegmentedButton segButton; - private SlideShowView slideShowPane; - @FXML private ToggleButton slideShowToggle; - - @FXML - private GridView gridView; - @FXML private ToggleButton tileToggle; + private SlideShowView slideShowPane; + + @FXML + private GridView gridView; @FXML private Button nextButton; - + @FXML + private AnchorPane nextButtonPane; + @FXML + private CheckBox seenByOtherExaminersCheckBox; @FXML private Button backButton; - @FXML private Button forwardButton; - @FXML private Label groupLabel; @FXML @@ -237,30 +230,27 @@ public class GroupPane extends BorderPane { @FXML private HBox catSplitMenuContainer; - private final KeyboardHandler tileKeyboardNavigationHandler = new KeyboardHandler(); - - private final NextUnseenGroup nextGroupAction; + private final ListeningExecutorService exec = TaskUtils.getExecutorForClass(GroupPane.class); private final ImageGalleryController controller; - private ContextMenu contextMenu; - + private final FileIDSelectionModel selectionModel; private Integer selectionAnchorIndex; + private final UndoAction undoAction; private final RedoAction redoAction; + private final Back backAction; + private final Forward forwardAction; + private final NextUnseenGroup nextGroupAction; - GroupViewMode getGroupViewMode() { - return groupViewMode.get(); - } + private final KeyboardHandler tileKeyboardNavigationHandler = new KeyboardHandler(); - /** - * the current GroupViewMode of this GroupPane - */ + private ContextMenu contextMenu; + + /** the current GroupViewMode of this GroupPane */ private final SimpleObjectProperty groupViewMode = new SimpleObjectProperty<>(GroupViewMode.TILE); - /** - * the grouping this pane is currently the view for - */ + /** the grouping this pane is currently the view for */ private final ReadOnlyObjectWrapper grouping = new ReadOnlyObjectWrapper<>(); /** @@ -294,6 +284,10 @@ public class GroupPane extends BorderPane { FXMLConstructor.construct(this, "GroupPane.fxml"); //NON-NLS } + GroupViewMode getGroupViewMode() { + return groupViewMode.get(); + } + @ThreadConfined(type = ThreadType.JFX) public void activateSlideShowViewer(Long slideShowFileID) { groupViewMode.set(GroupViewMode.SLIDE_SHOW); @@ -340,7 +334,9 @@ public class GroupPane extends BorderPane { } /** - * create the string to display in the group header + * Create the string to display in the group header. + * + * @return The string to display in the group header. */ @NbBundle.Messages({"# {0} - default group name", "# {1} - hashset hits count", @@ -391,19 +387,20 @@ public class GroupPane extends BorderPane { "GroupPane.catContainerLabel.displayText=Categorize Selected File:", "GroupPane.catHeadingLabel.displayText=Category:"}) void initialize() { - assert cat0Toggle != null : "fx:id=\"cat0Toggle\" was not injected: check your FXML file 'SlideShowView.fxml'."; - assert cat1Toggle != null : "fx:id=\"cat1Toggle\" was not injected: check your FXML file 'SlideShowView.fxml'."; - assert cat2Toggle != null : "fx:id=\"cat2Toggle\" was not injected: check your FXML file 'SlideShowView.fxml'."; - assert cat3Toggle != null : "fx:id=\"cat3Toggle\" was not injected: check your FXML file 'SlideShowView.fxml'."; - assert cat4Toggle != null : "fx:id=\"cat4Toggle\" was not injected: check your FXML file 'SlideShowView.fxml'."; - assert cat5Toggle != null : "fx:id=\"cat5Toggle\" was not injected: check your FXML file 'SlideShowView.fxml'."; + assert cat0Toggle != null : "fx:id=\"cat0Toggle\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert cat1Toggle != null : "fx:id=\"cat1Toggle\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert cat2Toggle != null : "fx:id=\"cat2Toggle\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert cat3Toggle != null : "fx:id=\"cat3Toggle\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert cat4Toggle != null : "fx:id=\"cat4Toggle\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert cat5Toggle != null : "fx:id=\"cat5Toggle\" was not injected: check your FXML file 'GroupPane.fxml'."; assert gridView != null : "fx:id=\"tilePane\" was not injected: check your FXML file 'GroupPane.fxml'."; - assert catSelectedSplitMenu != null : "fx:id=\"grpCatSplitMenu\" was not injected: check your FXML file 'GroupHeader.fxml'."; - assert tagSelectedSplitMenu != null : "fx:id=\"grpTagSplitMenu\" was not injected: check your FXML file 'GroupHeader.fxml'."; - assert headerToolBar != null : "fx:id=\"headerToolBar\" was not injected: check your FXML file 'GroupHeader.fxml'."; - assert segButton != null : "fx:id=\"previewList\" was not injected: check your FXML file 'GroupHeader.fxml'."; - assert slideShowToggle != null : "fx:id=\"segButton\" was not injected: check your FXML file 'GroupHeader.fxml'."; - assert tileToggle != null : "fx:id=\"tileToggle\" was not injected: check your FXML file 'GroupHeader.fxml'."; + assert catSelectedSplitMenu != null : "fx:id=\"grpCatSplitMenu\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert tagSelectedSplitMenu != null : "fx:id=\"grpTagSplitMenu\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert headerToolBar != null : "fx:id=\"headerToolBar\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert segButton != null : "fx:id=\"previewList\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert slideShowToggle != null : "fx:id=\"segButton\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert tileToggle != null : "fx:id=\"tileToggle\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert seenByOtherExaminersCheckBox != null : "fx:id=\"seenByOtherExaminersCheckBox\" was not injected: check your FXML file 'GroupPane.fxml'."; for (DhsImageCategory cat : DhsImageCategory.values()) { ToggleButton toggleForCategory = getToggleForCategory(cat); @@ -530,6 +527,16 @@ public class GroupPane extends BorderPane { } }); + seenByOtherExaminersCheckBox.selectedProperty().addListener((observable, oldValue, newValue) -> { + nextButtonPane.setDisable(true); + nextButtonPane.setCursor(Cursor.WAIT); + exec.submit(() -> controller.getGroupManager().setCollaborativeMode(newValue)) + .addListener(() -> { + nextButtonPane.setDisable(false); + nextButtonPane.setCursor(Cursor.DEFAULT); + }, Platform::runLater); + }); + //listen to tile selection and make sure it is visible in scroll area selectionModel.lastSelectedProperty().addListener((observable, oldFileID, newFileId) -> { if (groupViewMode.get() == GroupViewMode.SLIDE_SHOW @@ -607,7 +614,7 @@ public class GroupPane extends BorderPane { * assigns a grouping for this pane to represent and initializes grouping * specific properties and listeners * - * @param grouping the new grouping assigned to this group + * @param newViewState */ void setViewState(GroupViewState newViewState) { @@ -894,7 +901,7 @@ public class GroupPane extends BorderPane { if (t.getClickCount() == 1) { selectAllFiles(); } - if (selectionModel.getSelected().isEmpty() == false) { + if (isNotEmpty(selectionModel.getSelected())) { if (contextMenu == null) { contextMenu = buildContextMenu(); } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupCellFactory.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupCellFactory.java index d21b76d018..72f602d06a 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupCellFactory.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/GroupCellFactory.java @@ -173,7 +173,7 @@ class GroupCellFactory { private final InvalidationListener groupListener = new GroupListener<>(this); /** - * reference to group files listener that allows us to remove it from a + * Reference to group files listener that allows us to remove it from a * group when a new group is assigned to this Cell */ @Override From 2d4ae925de67cc8ae2c2d833f9605e908913b241 Mon Sep 17 00:00:00 2001 From: millmanorama Date: Sun, 16 Sep 2018 13:05:52 +0200 Subject: [PATCH 3/4] fix next unseen button state and text --- .../imagegallery/actions/NextUnseenGroup.java | 38 +- .../datamodel/grouping/GroupManager.java.orig | 888 ------------------ .../gui/drawableviews/GroupPane.fxml | 2 +- 3 files changed, 30 insertions(+), 898 deletions(-) delete mode 100644 ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java.orig diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/NextUnseenGroup.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/NextUnseenGroup.java index 22f0fcb12b..b53c36c91e 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/NextUnseenGroup.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/NextUnseenGroup.java @@ -39,7 +39,8 @@ import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState; */ @NbBundle.Messages({ "NextUnseenGroup.markGroupSeen=Mark Group Seen", - "NextUnseenGroup.nextUnseenGroup=Next Unseen group"}) + "NextUnseenGroup.nextUnseenGroup=Next Unseen group", + "NextUnseenGroup.allGroupsSeen=All Groups Have Been Seen"}) public class NextUnseenGroup extends Action { private static final String IMAGE_PATH = "/org/sleuthkit/autopsy/imagegallery/images/"; //NON-NLS @@ -50,6 +51,7 @@ public class NextUnseenGroup extends Action { private static final String MARK_GROUP_SEEN = Bundle.NextUnseenGroup_markGroupSeen(); private static final String NEXT_UNSEEN_GROUP = Bundle.NextUnseenGroup_nextUnseenGroup(); + private static final String ALL_GROUPS_SEEN = Bundle.NextUnseenGroup_allGroupsSeen(); private final ImageGalleryController controller; private final ObservableList unSeenGroups; @@ -63,6 +65,7 @@ public class NextUnseenGroup extends Action { groupManager = controller.getGroupManager(); unSeenGroups = groupManager.getUnSeenGroups(); unSeenGroups.addListener((Observable observable) -> updateButton()); + controller.viewStateProperty().addListener((Observable observable) -> updateButton()); setEventHandler(event -> { //on fx-thread //if there is a group assigned to the view, mark it as seen @@ -88,16 +91,33 @@ public class NextUnseenGroup extends Action { private void updateButton() { int size = unSeenGroups.size(); - Platform.runLater(() -> { - setDisabled(size == 0); - if (size <= 1) { - setText(MARK_GROUP_SEEN); - setGraphic(new ImageView(END_IMAGE)); + if (size < 1) { + //there are no unseen groups. + Platform.runLater(() -> { + setDisabled(true); + setText(ALL_GROUPS_SEEN); + setGraphic(null); + }); + } else { + DrawableGroup get = unSeenGroups.get(0); + DrawableGroup orElse = Optional.ofNullable(controller.getViewState()).flatMap(GroupViewState::getGroup).orElse(null); + boolean equals = get.equals(orElse); + if (size == 1 & equals) { + //The only unseen group is the one that is being viewed. + Platform.runLater(() -> { + setDisabled(false); + setText(MARK_GROUP_SEEN); + setGraphic(new ImageView(END_IMAGE)); + }); } else { - setText(NEXT_UNSEEN_GROUP); - setGraphic(new ImageView(ADVANCE_IMAGE)); + //there are more unseen groups. + Platform.runLater(() -> { + setDisabled(false); + setText(NEXT_UNSEEN_GROUP); + setGraphic(new ImageView(ADVANCE_IMAGE)); + }); } - }); + } } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java.orig b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java.orig deleted file mode 100644 index 668ef7baa4..0000000000 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java.orig +++ /dev/null @@ -1,888 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2013-18 Basis Technology Corp. - * Contact: carrier sleuthkit org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.sleuthkit.autopsy.imagegallery.datamodel.grouping; - -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; -import com.google.common.eventbus.Subscribe; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import static java.util.Objects.isNull; -import static java.util.Objects.nonNull; -import java.util.Optional; -import java.util.Set; -import java.util.TreeSet; -import java.util.concurrent.CancellationException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; -import java.util.logging.Level; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javafx.application.Platform; -import javafx.beans.property.ReadOnlyDoubleProperty; -import javafx.beans.property.ReadOnlyDoubleWrapper; -import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.ReadOnlyObjectWrapper; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import static javafx.concurrent.Worker.State.CANCELLED; -import static javafx.concurrent.Worker.State.FAILED; -import static javafx.concurrent.Worker.State.READY; -import static javafx.concurrent.Worker.State.RUNNING; -import static javafx.concurrent.Worker.State.SCHEDULED; -import static javafx.concurrent.Worker.State.SUCCEEDED; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; -import javax.swing.SortOrder; -import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.concurrent.BasicThreadFactory; -import org.netbeans.api.progress.ProgressHandle; -import org.openide.util.Exceptions; -import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; -import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; -import org.sleuthkit.autopsy.coreutils.LoggedTask; -import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.datamodel.DhsImageCategory; -import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; -import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager; -import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute; -import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableDB; -import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile; -import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableTagsManager; -import org.sleuthkit.datamodel.AbstractFile; -import org.sleuthkit.datamodel.ContentTag; -import org.sleuthkit.datamodel.DataSource; -import org.sleuthkit.datamodel.SleuthkitCase; -import org.sleuthkit.datamodel.TagName; -import org.sleuthkit.datamodel.TskCoreException; -import org.sleuthkit.datamodel.TskData.DbType; -import org.sleuthkit.datamodel.TskDataException; - -/** - * Provides an abstraction layer on top of DrawableDB ( and to some extent - * SleuthkitCase ) to facilitate creation, retrieval, updating, and sorting of - * DrawableGroups. - */ -public class GroupManager { - - private static final Logger logger = Logger.getLogger(GroupManager.class.getName()); - - /** An executor to submit async UI related background tasks to. */ - private final ListeningExecutorService exec = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor( - new BasicThreadFactory.Builder().namingPattern("GUI Task -%d").build())); //NON-NLS - - private final ImageGalleryController controller; - - /** list of all analyzed groups */ - @GuardedBy("this") - private final ObservableList analyzedGroups = FXCollections.observableArrayList(); - private final ObservableList unmodifiableAnalyzedGroups = FXCollections.unmodifiableObservableList(analyzedGroups); - - /** list of unseen groups */ - @GuardedBy("this") - private final ObservableList unSeenGroups = FXCollections.observableArrayList(); - private final ObservableList unmodifiableUnSeenGroups = FXCollections.unmodifiableObservableList(unSeenGroups); - /** - * map from GroupKey} to DrawableGroupSs. All groups (even not fully - * analyzed or not visible groups could be in this map - */ - @GuardedBy("this") - private final Map, DrawableGroup> groupMap = new HashMap<>(); - - @GuardedBy("this") - private ReGroupTask groupByTask; - - /* - * --- current grouping/sorting attributes --- - */ - @GuardedBy("this") - private final ReadOnlyObjectWrapper< GroupSortBy> sortByProp = new ReadOnlyObjectWrapper<>(GroupSortBy.PRIORITY); - private final ReadOnlyObjectWrapper< DrawableAttribute> groupByProp = new ReadOnlyObjectWrapper<>(DrawableAttribute.PATH); - private final ReadOnlyObjectWrapper sortOrderProp = new ReadOnlyObjectWrapper<>(SortOrder.ASCENDING); - private final ReadOnlyObjectWrapper dataSourceProp = new ReadOnlyObjectWrapper<>(null);//null indicates all datasources - - private final ReadOnlyDoubleWrapper regroupProgress = new ReadOnlyDoubleWrapper(); - -<<<<<<< HEAD - synchronized DrawableDB getDB() { -======= - public void setDB(DrawableDB db) { - regroup(dataSource, groupBy, sortBy, sortOrder, true); - } - - private DrawableDB getDB() { ->>>>>>> 1010/7-datasource-filtering-test - return controller.getDatabase(); - } - - @SuppressWarnings("ReturnOfCollectionOrArrayField") - public ObservableList getAnalyzedGroups() { - return unmodifiableAnalyzedGroups; - } - - @SuppressWarnings("ReturnOfCollectionOrArrayField") - public ObservableList getUnSeenGroups() { - return unmodifiableUnSeenGroups; - } - - /** - * construct a group manager hooked up to the given db and controller - * - * @param controller - */ - public GroupManager(ImageGalleryController controller) { - this.controller = controller; - } - - /** - * Using the current groupBy set for this manager, find groupkeys for all - * the groups the given file is a part of - * - * @param file - * - * - * @return A a set of GroupKeys representing the group(s) the given file is - * a part of. - */ - @SuppressWarnings({"rawtypes", "unchecked"}) - synchronized public Set> getGroupKeysForFile(DrawableFile file) { - Set> resultSet = new HashSet<>(); - for (Comparable val : getGroupBy().getValue(file)) { - if (getGroupBy() == DrawableAttribute.TAGS) { - if (CategoryManager.isNotCategoryTagName((TagName) val)) { - resultSet.add(new GroupKey(getGroupBy(), val, getDataSource())); - } - } else { - resultSet.add(new GroupKey(getGroupBy(), val, getDataSource())); - } - } - return resultSet; - } - - /** - * Using the current grouping paramaters set for this manager, find - * GroupKeys for all the Groups the given file is a part of. - * - * @param fileID The Id of the file to get group keys for. - * - * @return A set of GroupKeys representing the group(s) the given file is a - * part of - */ - synchronized public Set> getGroupKeysForFileID(Long fileID) { - try { - DrawableDB db = getDB(); - if (nonNull(db)) { - DrawableFile file = db.getFileFromID(fileID); - return getGroupKeysForFile(file); - } else { - Logger.getLogger(GroupManager.class.getName()).log(Level.WARNING, "Failed to load file with id: {0} from database. There is no database assigned.", fileID); //NON-NLS - } - } catch (TskCoreException ex) { - Logger.getLogger(GroupManager.class.getName()).log(Level.SEVERE, "failed to load file with id: " + fileID + " from database", ex); //NON-NLS - } - return Collections.emptySet(); - } - - /** - * @param groupKey - * - * @return return the DrawableGroup (if it exists) for the given GroupKey, - * or null if no group exists for that key. - */ - @Nullable - synchronized public DrawableGroup getGroupForKey(@Nonnull GroupKey groupKey) { - return groupMap.get(groupKey); - } - - synchronized public void reset() { - if (groupByTask != null) { - groupByTask.cancel(true); - } - setSortBy(GroupSortBy.GROUP_BY_VALUE); - setGroupBy(DrawableAttribute.PATH); - setSortOrder(SortOrder.ASCENDING); - setDataSource(null); - - unSeenGroups.forEach(controller.getCategoryManager()::unregisterListener); - unSeenGroups.clear(); - analyzedGroups.forEach(controller.getCategoryManager()::unregisterListener); - analyzedGroups.clear(); - - groupMap.values().forEach(controller.getCategoryManager()::unregisterListener); - groupMap.clear(); - } - - synchronized public boolean isRegrouping() { - if (groupByTask == null) { - return false; - } - - switch (groupByTask.getState()) { - case READY: - case RUNNING: - case SCHEDULED: - return true; - case CANCELLED: - case FAILED: - case SUCCEEDED: - default: - return false; - } - } - - /** - * 'Save' the given group as seen in the drawable db. - * - * @param group The DrawableGroup to mark as seen. - * @param seen The seen state to set for the given group. - * - * @return A ListenableFuture that encapsulates saving the seen state to the - * DB. - */ - synchronized public ListenableFuture setGroupSeen(DrawableGroup group, boolean seen) { - DrawableDB db = getDB(); - if (nonNull(db)) { - return exec.submit(() -> { - try { - - db.setGroupSeen(group.getGroupKey(), seen); - group.setSeen(seen); - updateUnSeenGroups(group, seen); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error marking group as seen", ex); //NON-NLS - } - }); - } - - return Futures.immediateFuture(null); - } - - synchronized private void updateUnSeenGroups(DrawableGroup group, boolean seen) { - if (seen) { - unSeenGroups.removeAll(group); - } else if (unSeenGroups.contains(group) == false) { - unSeenGroups.add(group); - } - sortUnseenGroups(); - } - - /** - * remove the given file from the group with the given key. If the group - * doesn't exist or doesn't already contain this file, this method is a - * no-op - * - * @param groupKey the value of groupKey - * @param fileID the value of file - * - * @return The DrawableGroup the file was removed from. - * - */ - public synchronized DrawableGroup removeFromGroup(GroupKey groupKey, final Long fileID) { - //get grouping this file would be in - final DrawableGroup group = getGroupForKey(groupKey); - if (group != null) { - synchronized (group) { - group.removeFile(fileID); - - // If we're grouping by category, we don't want to remove empty groups. - if (groupKey.getAttribute() != DrawableAttribute.CATEGORY) { - if (group.getFileIDs().isEmpty()) { - if (analyzedGroups.contains(group)) { - analyzedGroups.remove(group); - sortAnalyzedGroups(); - } - if (unSeenGroups.contains(group)) { - unSeenGroups.remove(group); - sortUnseenGroups(); - } - - } - } - return group; - } - } else { //group == null - // It may be that this was the last unanalyzed file in the group, so test - // whether the group is now fully analyzed. - return popuplateIfAnalyzed(groupKey, null); - } - } - - synchronized private void sortUnseenGroups() { - FXCollections.sort(unSeenGroups, makeGroupComparator(getSortOrder(), getSortBy())); - } - - synchronized private void sortAnalyzedGroups() { - FXCollections.sort(analyzedGroups, makeGroupComparator(getSortOrder(), getSortBy())); - } - - synchronized public Set getFileIDsInGroup(GroupKey groupKey) throws TskCoreException { - Set fileIDsToReturn = Collections.emptySet(); - switch (groupKey.getAttribute().attrName) { - //these cases get special treatment - case CATEGORY: - fileIDsToReturn = getFileIDsWithCategory((DhsImageCategory) groupKey.getValue()); - break; - case TAGS: - fileIDsToReturn = getFileIDsWithTag((TagName) groupKey.getValue()); - break; - case MIME_TYPE: - fileIDsToReturn = getFileIDsWithMimeType((String) groupKey.getValue()); - break; -// case HASHSET: //comment out this case to use db functionality for hashsets -// return getFileIDsWithHashSetName((String) groupKey.getValue()); - default: - DrawableDB db = getDB(); - //straight db query - if (nonNull(db)) { - fileIDsToReturn = db.getFileIDsInGroup(groupKey); - } - } - return fileIDsToReturn; - } - - // @@@ This was kind of slow in the profiler. Maybe we should cache it. - // Unless the list of file IDs is necessary, use countFilesWithCategory() to get the counts. - synchronized public Set getFileIDsWithCategory(DhsImageCategory category) throws TskCoreException { - Set fileIDsToReturn = Collections.emptySet(); - DrawableDB db = getDB(); - if (nonNull(db)) { - try { - final DrawableTagsManager tagsManager = controller.getTagsManager(); - if (category == DhsImageCategory.ZERO) { - List< TagName> tns = Stream.of(DhsImageCategory.ONE, DhsImageCategory.TWO, - DhsImageCategory.THREE, DhsImageCategory.FOUR, DhsImageCategory.FIVE) - .map(tagsManager::getTagName) - .collect(Collectors.toList()); - - Set files = new HashSet<>(); - for (TagName tn : tns) { - if (tn != null) { - List contentTags = tagsManager.getContentTagsByTagName(tn); - files.addAll(contentTags.stream() - .filter(ct -> ct.getContent() instanceof AbstractFile) - .filter(ct -> db.isInDB(ct.getContent().getId())) - .map(ct -> ct.getContent().getId()) - .collect(Collectors.toSet())); - } - } - - fileIDsToReturn = db.findAllFileIdsWhere("obj_id NOT IN (" + StringUtils.join(files, ',') + ")"); //NON-NLS - } else { - - List contentTags = tagsManager.getContentTagsByTagName(tagsManager.getTagName(category)); - fileIDsToReturn = contentTags.stream() - .filter(ct -> ct.getContent() instanceof AbstractFile) - .filter(ct -> db.isInDB(ct.getContent().getId())) - .map(ct -> ct.getContent().getId()) - .collect(Collectors.toSet()); - } - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "TSK error getting files in Category:" + category.getDisplayName(), ex); //NON-NLS - throw ex; - } - } - return fileIDsToReturn; - } - - synchronized public Set getFileIDsWithTag(TagName tagName) throws TskCoreException { - Set files = new HashSet<>(); - try { - - List contentTags = controller.getTagsManager().getContentTagsByTagName(tagName); - DrawableDB db = getDB(); - for (ContentTag ct : contentTags) { - if (ct.getContent() instanceof AbstractFile && nonNull(db) && db.isInDB(ct.getContent().getId())) { - files.add(ct.getContent().getId()); - } - } - return files; - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "TSK error getting files with Tag:" + tagName.getDisplayName(), ex); //NON-NLS - throw ex; - } - } - - public synchronized GroupSortBy getSortBy() { - return sortByProp.get(); - } - - synchronized void setSortBy(GroupSortBy sortBy) { - sortByProp.set(sortBy); - } - - public ReadOnlyObjectProperty< GroupSortBy> getSortByProperty() { - return sortByProp.getReadOnlyProperty(); - } - - public synchronized DrawableAttribute getGroupBy() { - return groupByProp.get(); - } - - synchronized void setGroupBy(DrawableAttribute groupBy) { - groupByProp.set(groupBy); - } - - public ReadOnlyObjectProperty> getGroupByProperty() { - return groupByProp.getReadOnlyProperty(); - } - - public synchronized SortOrder getSortOrder() { - return sortOrderProp.get(); - } - - synchronized void setSortOrder(SortOrder sortOrder) { - sortOrderProp.set(sortOrder); - } - - public ReadOnlyObjectProperty getSortOrderProperty() { - return sortOrderProp.getReadOnlyProperty(); - } - - public synchronized DataSource getDataSource() { - return dataSourceProp.get(); - } - - synchronized void setDataSource(DataSource dataSource) { - dataSourceProp.set(dataSource); - } - - public ReadOnlyObjectProperty getDataSourceProperty() { - return dataSourceProp.getReadOnlyProperty(); - } - - /** - * Regroup all files in the database. see ReGroupTask for more details. - * - * @param The type of the values of the groupBy attriubte. - * @param dataSource The DataSource to show. Null for all data sources. - * @param groupBy The DrawableAttribute to group by - * @param sortBy The GroupSortBy to sort the groups by - * @param sortOrder The SortOrder to use when sorting the groups. - * @param force true to force a full db query regroup, even if only the - * sorting has changed. - */ - public synchronized > void regroup(DataSource dataSource, DrawableAttribute groupBy, GroupSortBy sortBy, SortOrder sortOrder, Boolean force) { - - if (!Case.isCaseOpen()) { - return; - } - - //only re-query the db if the data source or group by attribute changed or it is forced - if (dataSource != getDataSource() - || groupBy != getGroupBy() - || force) { - - setDataSource(dataSource); - setGroupBy(groupBy); - setSortBy(sortBy); - setSortOrder(sortOrder); - if (groupByTask != null) { - groupByTask.cancel(true); - } - - groupByTask = new ReGroupTask<>(dataSource, groupBy, sortBy, sortOrder); - Platform.runLater(() -> regroupProgress.bind(groupByTask.progressProperty())); - exec.submit(groupByTask); - } else { - // resort the list of groups - setSortBy(sortBy); - setSortOrder(sortOrder); - Platform.runLater(() -> { - FXCollections.sort(analyzedGroups, makeGroupComparator(sortOrder, sortBy)); - FXCollections.sort(unSeenGroups, makeGroupComparator(sortOrder, sortBy)); - }); - } - } - - public ReadOnlyDoubleProperty regroupProgress() { - return regroupProgress.getReadOnlyProperty(); - } - - @Subscribe - synchronized public void handleTagAdded(ContentTagAddedEvent evt) { - GroupKey newGroupKey = null; - final long fileID = evt.getAddedTag().getContent().getId(); - if (getGroupBy() == DrawableAttribute.CATEGORY && CategoryManager.isCategoryTagName(evt.getAddedTag().getName())) { - newGroupKey = new GroupKey<>(DrawableAttribute.CATEGORY, CategoryManager.categoryFromTagName(evt.getAddedTag().getName()), getDataSource()); - for (GroupKey oldGroupKey : groupMap.keySet()) { - if (oldGroupKey.equals(newGroupKey) == false) { - removeFromGroup(oldGroupKey, fileID); - } - } - } else if (getGroupBy() == DrawableAttribute.TAGS && CategoryManager.isNotCategoryTagName(evt.getAddedTag().getName())) { - newGroupKey = new GroupKey<>(DrawableAttribute.TAGS, evt.getAddedTag().getName(), getDataSource()); - } - if (newGroupKey != null) { - DrawableGroup g = getGroupForKey(newGroupKey); - addFileToGroup(g, newGroupKey, fileID); - } - } - - @SuppressWarnings("AssignmentToMethodParameter") - synchronized private void addFileToGroup(DrawableGroup g, final GroupKey groupKey, final long fileID) { - if (g == null) { - //if there wasn't already a group check if there should be one now - g = popuplateIfAnalyzed(groupKey, null); - } - DrawableGroup group = g; - if (group != null) { - //if there is aleady a group that was previously deemed fully analyzed, then add this newly analyzed file to it. - group.addFile(fileID); - } - } - - @Subscribe - synchronized public void handleTagDeleted(ContentTagDeletedEvent evt) { - GroupKey groupKey = null; - final ContentTagDeletedEvent.DeletedContentTagInfo deletedTagInfo = evt.getDeletedTagInfo(); - final TagName tagName = deletedTagInfo.getName(); - if (getGroupBy() == DrawableAttribute.CATEGORY && CategoryManager.isCategoryTagName(tagName)) { - groupKey = new GroupKey<>(DrawableAttribute.CATEGORY, CategoryManager.categoryFromTagName(tagName), getDataSource()); - } else if (getGroupBy() == DrawableAttribute.TAGS && CategoryManager.isNotCategoryTagName(tagName)) { - groupKey = new GroupKey<>(DrawableAttribute.TAGS, tagName, getDataSource()); - } - if (groupKey != null) { - final long fileID = deletedTagInfo.getContentID(); - DrawableGroup g = removeFromGroup(groupKey, fileID); - } - } - - @Subscribe - synchronized public void handleFileRemoved(Collection removedFileIDs) { - - for (final long fileId : removedFileIDs) { - //get grouping(s) this file would be in - Set> groupsForFile = getGroupKeysForFileID(fileId); - - for (GroupKey gk : groupsForFile) { - removeFromGroup(gk, fileId); - } - } - } - - /** - * Handle notifications sent from Db when files are inserted/updated - * - * @param updatedFileIDs The ID of the inserted/updated files. - */ - @Subscribe - synchronized public void handleFileUpdate(Collection updatedFileIDs) { - /** - * TODO: is there a way to optimize this to avoid quering to db so much. - * the problem is that as a new files are analyzed they might be in new - * groups( if we are grouping by say make or model) -jm - */ - for (long fileId : updatedFileIDs) { - - controller.getHashSetManager().invalidateHashSetsForFile(fileId); - - //get grouping(s) this file would be in - Set> groupsForFile = getGroupKeysForFileID(fileId); - for (GroupKey gk : groupsForFile) { - DrawableGroup g = getGroupForKey(gk); - addFileToGroup(g, gk, fileId); - } - } - - //we fire this event for all files so that the category counts get updated during initial db population - controller.getCategoryManager().fireChange(updatedFileIDs, null); - } - - synchronized private DrawableGroup popuplateIfAnalyzed(GroupKey groupKey, ReGroupTask task) { - /* - * If this method call is part of a ReGroupTask and that task is - * cancelled, no-op. - * - * This allows us to stop if a regroup task has been cancelled (e.g. the - * user picked a different group by attribute, while the current task - * was still running) - */ - if (isNull(task) || task.isCancelled() == false) { - DrawableDB db = getDB(); - /* - * For attributes other than path we can't be sure a group is fully - * analyzed because we don't know all the files that will be a part - * of that group. just show them no matter what. - */ - if (nonNull(db) && ((groupKey.getAttribute() != DrawableAttribute.PATH) || db.isGroupAnalyzed(groupKey))) { - try { - Set fileIDs = getFileIDsInGroup(groupKey); - if (Objects.nonNull(fileIDs)) { - DrawableGroup group; - final boolean groupSeen = db.isGroupSeen(groupKey); - if (groupMap.containsKey(groupKey)) { - group = groupMap.get(groupKey); - group.setFiles(ObjectUtils.defaultIfNull(fileIDs, Collections.emptySet())); - group.setSeen(groupSeen); - } else { - group = new DrawableGroup(groupKey, fileIDs, groupSeen); - controller.getCategoryManager().registerListener(group); - groupMap.put(groupKey, group); - } - - if (analyzedGroups.contains(group) == false) { - analyzedGroups.add(group); - if (isNull(task)) { - sortAnalyzedGroups(); - } - } - updateUnSeenGroups(group, groupSeen); - - return group; - - } - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "failed to get files for group: " + groupKey.getAttribute().attrName.toString() + " = " + groupKey.getValue(), ex); //NON-NLS - } - } - } - - return null; - } - - synchronized public Set getFileIDsWithMimeType(String mimeType) throws TskCoreException { - - HashSet hashSet = new HashSet<>(); - String query = (null == mimeType) - ? "SELECT obj_id FROM tsk_files WHERE mime_type IS NULL" //NON-NLS - : "SELECT obj_id FROM tsk_files WHERE mime_type = '" + mimeType + "'"; //NON-NLS - DrawableDB db = getDB(); - try (SleuthkitCase.CaseDbQuery executeQuery = controller.getSleuthKitCase().executeQuery(query); - ResultSet resultSet = executeQuery.getResultSet();) { - while (resultSet.next()) { - final long fileID = resultSet.getLong("obj_id"); //NON-NLS - if (nonNull(db) && db.isInDB(fileID)) { - hashSet.add(fileID); - } - } - return hashSet; - - } catch (Exception ex) { - throw new TskCoreException("Failed to get file ids with mime type " + mimeType, ex); - } - } - - /** - * Task to query database for files in sorted groups and build - * DrawableGroups for them. - */ - @SuppressWarnings({"unchecked", "rawtypes"}) - @NbBundle.Messages({"# {0} - groupBy attribute Name", - "# {1} - sortBy name", - "# {2} - sort Order", - "ReGroupTask.displayTitle=regrouping files by {0} sorted by {1} in {2} order", - "# {0} - groupBy attribute Name", - "# {1} - atribute value", - "ReGroupTask.progressUpdate=regrouping files by {0} : {1}"}) - private class ReGroupTask> extends LoggedTask { - - private final DataSource dataSource; - private final DrawableAttribute groupBy; - private final GroupSortBy sortBy; - private final SortOrder sortOrder; - - private final ProgressHandle groupProgress; - - ReGroupTask(DataSource dataSource, DrawableAttribute groupBy, GroupSortBy sortBy, SortOrder sortOrder) { - super(Bundle.ReGroupTask_displayTitle(groupBy.attrName.toString(), sortBy.getDisplayName(), sortOrder.toString()), true); - this.dataSource = dataSource; - this.groupBy = groupBy; - this.sortBy = sortBy; - this.sortOrder = sortOrder; - - groupProgress = ProgressHandle.createHandle(Bundle.ReGroupTask_displayTitle(groupBy.attrName.toString(), sortBy.getDisplayName(), sortOrder.toString()), this); - } - - @Override - public boolean isCancelled() { - return super.isCancelled(); - } - - @Override - protected Void call() throws Exception { - - if (isCancelled()) { - return null; - } - groupProgress.start(); - - synchronized (GroupManager.this) { - analyzedGroups.clear(); - unSeenGroups.clear(); - - // Get the list of group keys - final Multimap valsByDataSource = findValuesForAttribute(); - groupProgress.switchToDeterminate(valsByDataSource.size()); - int p = 0; - // For each key value, partially create the group and add it to the list. - for (final Map.Entry val : valsByDataSource.entries()) { - if (isCancelled()) { - return null; - } - p++; - updateMessage(Bundle.ReGroupTask_progressUpdate(groupBy.attrName.toString(), val.getValue())); - updateProgress(p, valsByDataSource.size()); - groupProgress.progress(Bundle.ReGroupTask_progressUpdate(groupBy.attrName.toString(), val), p); - popuplateIfAnalyzed(new GroupKey<>(groupBy, val.getValue(), val.getKey()), this); - } - } - - DataSource dataSourceOfCurrentGroup - = Optional.ofNullable(controller.getViewState()) - .flatMap(GroupViewState::getGroup) - .map(DrawableGroup::getGroupKey) - .flatMap(GroupKey::getDataSource) - .orElse(null); - if (getDataSource() == null - || Objects.equals(dataSourceOfCurrentGroup, getDataSource())) { - //the current group is for the given datasource, so just keep it in view. - } else { //the current group should not be visible so ... - if (isNotEmpty(unSeenGroups)) {// show then next unseen group - controller.advance(GroupViewState.tile(unSeenGroups.get(0)), false); - } else { // clear the group area. - controller.advance(GroupViewState.tile(null), false); - } - } - - groupProgress.finish(); - updateProgress(1, 1); - return null; - } - - @Override - protected void done() { - super.done(); - try { - get(); - } catch (CancellationException cancelEx) { - //cancellation is normal - } catch (InterruptedException | ExecutionException ex) { - logger.log(Level.SEVERE, "Error while regrouping.", ex); - } - } - - /** - * find the distinct values for the given column (DrawableAttribute) - * - * These values represent the groups of files. - * - * @param groupBy - * - * @return - */ - public Multimap findValuesForAttribute() { - synchronized (GroupManager.this) { - DrawableDB db = getDB(); - Multimap results = HashMultimap.create(); - try { - switch (groupBy.attrName) { - //these cases get special treatment - case CATEGORY: - results.putAll(null, Arrays.asList(DhsImageCategory.values())); - break; - case TAGS: - results.putAll(null, controller.getTagsManager().getTagNamesInUse().stream() - .filter(CategoryManager::isNotCategoryTagName) - .collect(Collectors.toList())); - break; - - case ANALYZED: - results.putAll(null, Arrays.asList(false, true)); - break; - case HASHSET: - if (nonNull(db)) { - results.putAll(null, new TreeSet<>(db.getHashSetNames())); - } - break; - case MIME_TYPE: - if (nonNull(db)) { - HashSet types = new HashSet<>(); - - // Use the group_concat function to get a list of files for each mime type. - // This has different syntax on Postgres vs SQLite - String groupConcatClause; - if (DbType.POSTGRESQL == controller.getSleuthKitCase().getDatabaseType()) { - groupConcatClause = " array_to_string(array_agg(obj_id), ',') as object_ids"; - } else { - groupConcatClause = " group_concat(obj_id) as object_ids"; - } - String query = "select " + groupConcatClause + " , mime_type from tsk_files group by mime_type "; - try (SleuthkitCase.CaseDbQuery executeQuery = controller.getSleuthKitCase().executeQuery(query); //NON-NLS - ResultSet resultSet = executeQuery.getResultSet();) { - while (resultSet.next()) { - final String mimeType = resultSet.getString("mime_type"); //NON-NLS - String objIds = resultSet.getString("object_ids"); //NON-NLS - - Pattern.compile(",").splitAsStream(objIds) - .map(Long::valueOf) - .filter(db::isInDB) - .findAny().ifPresent(obj_id -> types.add(mimeType)); - } - } catch (SQLException | TskCoreException ex) { - Exceptions.printStackTrace(ex); - } - results.putAll(null, types); - } - break; - default: - //otherwise do straight db query - if (nonNull(db)) { - results.putAll(db.findValuesForAttribute(groupBy, sortBy, sortOrder, dataSource)); - } - } - - } catch (TskCoreException | TskDataException ex) { - logger.log(Level.SEVERE, "TSK error getting list of type {0}", groupBy.getDisplayName()); //NON-NLS - } - return results; - } - } - } - - private static Comparator makeGroupComparator(final SortOrder sortOrder, GroupSortBy comparator) { - switch (sortOrder) { - case ASCENDING: - return comparator; - case DESCENDING: - return comparator.reversed(); - case UNSORTED: - default: - return new GroupSortBy.AllEqualComparator<>(); - } - } -} diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.fxml b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.fxml index fc286f3c7b..0e788327c4 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.fxml +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.fxml @@ -64,7 +64,7 @@ -