From 12851c71b83c6480679719aca6a400488a2e3df5 Mon Sep 17 00:00:00 2001 From: millmanorama Date: Thu, 4 Oct 2018 17:33:29 +0200 Subject: [PATCH 1/4] make sure all db queries for widget initialization happen on bg threads. --- .../imagegallery/gui/SummaryTablePane.java | 2 +- .../autopsy/imagegallery/gui/Toolbar.java | 96 ++++++++----------- .../gui/drawableviews/GroupPane.java | 70 ++++++-------- .../gui/drawableviews/MetaDataPane.java | 14 +-- .../gui/drawableviews/SlideShowView.java | 19 +--- .../gui/navpanel/HashHitGroupList.java | 2 +- .../autopsy/imagegallery/utils/TaskUtils.java | 25 ++++- 7 files changed, 108 insertions(+), 120 deletions(-) diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SummaryTablePane.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SummaryTablePane.java index a9c4da2907..4884f580b7 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SummaryTablePane.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/SummaryTablePane.java @@ -82,7 +82,7 @@ public class SummaryTablePane extends AnchorPane { //register for category events controller.getCategoryManager().registerListener(this); - handleCategoryChanged(null); + new Thread(() -> handleCategoryChanged(null)).start(); } public SummaryTablePane(ImageGalleryController controller) { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/Toolbar.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/Toolbar.java index f1c4ac51bf..6ce152bfdb 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/Toolbar.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/Toolbar.java @@ -74,6 +74,7 @@ import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupSortBy; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState; import org.sleuthkit.autopsy.imagegallery.utils.TaskUtils; +import static org.sleuthkit.autopsy.imagegallery.utils.TaskUtils.addFXCallback; import org.sleuthkit.datamodel.DataSource; /** @@ -255,6 +256,7 @@ public class Toolbar extends ToolBar { evt -> { Platform.runLater(() -> { Optional selectedItem = dataSourceSelectionModel.getSelectedItem(); + //restore selection once the sync is done. syncDataSources().addListener(() -> dataSourceSelectionModel.select(selectedItem), Platform::runLater); }); }); @@ -269,33 +271,30 @@ public class Toolbar extends ToolBar { } private void initTagMenuButton() { - ListenableFuture future = exec.submit(() -> new TagGroupAction(controller.getTagsManager().getFollowUpTagName(), controller)); - Futures.addCallback(future, new FutureCallback() { - @Override - public void onSuccess(TagGroupAction followUpGroupAction) { - tagGroupMenuButton.setOnAction(followUpGroupAction); - tagGroupMenuButton.setText(followUpGroupAction.getText()); - tagGroupMenuButton.setGraphic(followUpGroupAction.getGraphic()); - } - - @Override - public void onFailure(Throwable throwable) { - /* - * The problem appears to be a timing issue where a case is - * closed before this initialization is completed, which It - * appears to be harmless, so we are temporarily changing this - * log message to a WARNING. - * - * TODO (JIRA-3010): SEVERE error logged by image Gallery UI - */ - if (Case.isCaseOpen()) { - logger.log(Level.WARNING, "Could not create Follow Up tag menu item", throwable); //NON-NLS - } else { - // don't add stack trace to log because it makes looking for real errors harder - logger.log(Level.INFO, "Unable to get tag name. Case is closed."); //NON-NLS + addFXCallback(exec.submit(() -> new TagGroupAction(controller.getTagsManager().getFollowUpTagName(), controller)), + followUpGroupAction -> { + //on fx thread + tagGroupMenuButton.setOnAction(followUpGroupAction); + tagGroupMenuButton.setText(followUpGroupAction.getText()); + tagGroupMenuButton.setGraphic(followUpGroupAction.getGraphic()); + }, + throwable -> { + /* + * The problem appears to be a timing issue where a case is + * closed before this initialization is completed, which It + * appears to be harmless, so we are temporarily changing + * this log message to a WARNING. + * + * TODO (JIRA-3010): SEVERE error logged by image Gallery UI + */ + if (Case.isCaseOpen()) { + logger.log(Level.WARNING, "Could not create Follow Up tag menu item", throwable); //NON-NLS + } else { + // don't add stack trace to log because it makes looking for real errors harder + logger.log(Level.INFO, "Unable to get tag name. Case is closed."); //NON-NLS + } } - } - }, Platform::runLater); + ); tagGroupMenuButton.showingProperty().addListener(showing -> { if (tagGroupMenuButton.isShowing()) { @@ -303,24 +302,18 @@ public class Toolbar extends ToolBar { return Lists.transform(controller.getTagsManager().getNonCategoryTagNames(), tagName -> GuiUtils.createAutoAssigningMenuItem(tagGroupMenuButton, new TagGroupAction(tagName, controller))); }); - Futures.addCallback(getTagsFuture, new FutureCallback>() { - @Override - public void onSuccess(List result) { - tagGroupMenuButton.getItems().setAll(result); - } - @Override - public void onFailure(Throwable t) { - logger.log(Level.SEVERE, "Error getting non-gategory tag names.", t); - } - }, Platform::runLater); + addFXCallback(getTagsFuture, + menuItems -> tagGroupMenuButton.getItems().setAll(menuItems), + throwable -> logger.log(Level.SEVERE, "Error getting non-gategory tag names.", throwable) + ); } }); } @ThreadConfined(type = ThreadConfined.ThreadType.ANY) private ListenableFuture> syncDataSources() { - ListenableFuture> future = exec.submit(() -> { + ListenableFuture> dataSourcesFuture = exec.submit(() -> { List dataSourcesInCase = controller.getSleuthKitCase().getDataSources(); synchronized (dataSourcesViewable) { dataSourcesViewable.clear(); @@ -331,22 +324,18 @@ public class Toolbar extends ToolBar { } return dataSourcesInCase; }); - Futures.addCallback(future, new FutureCallback>() { - @Override - public void onSuccess(List result) { - List> newDataSources = new ArrayList<>(); - newDataSources.add(Optional.empty()); - result.forEach(dataSource -> newDataSources.add(Optional.of(dataSource))); - dataSources.setAll(newDataSources); - } + addFXCallback(dataSourcesFuture, + result -> { + //on fx thread + List> newDataSources = new ArrayList<>(Lists.transform(result, Optional::of)); + newDataSources.add(0, Optional.empty()); + dataSources.setAll(newDataSources); + }, + throwable -> logger.log(Level.SEVERE, "Unable to get datasources for current case.", throwable) //NON-NLS - @Override - public void onFailure(Throwable t) { - logger.log(Level.SEVERE, "Unable to get datasources for current case.", t); //NON-NLS - } - }, Platform::runLater); + ); - return future; + return dataSourcesFuture; } /** @@ -384,8 +373,7 @@ public class Toolbar extends ToolBar { * selection. */ private void syncGroupControlsEnabledState(GroupViewState newViewState) { - boolean noGroupSelected = (null == newViewState) - || (null == newViewState.getGroup()); + boolean noGroupSelected = (null == newViewState) || (null == newViewState.getGroup()); Platform.runLater(() -> { tagGroupMenuButton.setDisable(noGroupSelected); catGroupMenuButton.setDisable(noGroupSelected); @@ -402,7 +390,5 @@ public class Toolbar extends ToolBar { public Toolbar(ImageGalleryController controller) { this.controller = controller; FXMLConstructor.construct(this, "Toolbar.fxml"); //NON-NLS - } - } 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 12fe8b576d..7055874093 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java @@ -21,12 +21,12 @@ package org.sleuthkit.autopsy.imagegallery.gui.drawableviews; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; +import static com.google.common.collect.Lists.transform; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import java.util.ArrayList; import java.util.Arrays; +import static java.util.Arrays.asList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -35,6 +35,7 @@ import java.util.Map; import static java.util.Objects.isNull; import static java.util.Objects.nonNull; import java.util.Optional; +import java.util.function.Consumer; import java.util.logging.Level; import java.util.stream.IntStream; import javafx.animation.Interpolator; @@ -135,7 +136,10 @@ import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.DrawableGroup; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewMode; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState; import org.sleuthkit.autopsy.imagegallery.gui.GuiUtils; +import static org.sleuthkit.autopsy.imagegallery.gui.GuiUtils.createAutoAssigningMenuItem; import org.sleuthkit.autopsy.imagegallery.utils.TaskUtils; +import static org.sleuthkit.autopsy.imagegallery.utils.TaskUtils.addFXCallback; +import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.TskCoreException; /** @@ -254,7 +258,7 @@ public class GroupPane extends BorderPane { private final ReadOnlyObjectWrapper grouping = new ReadOnlyObjectWrapper<>(); /** - * map from fileIDs to their assigned cells in the tile view. This is used + * Map from fileIDs to their assigned cells in the tile view. This is used * to determine whether fileIDs are visible or are offscreen. No entry * indicates the given fileID is not displayed on screen. DrawableCells are * responsible for adding and removing themselves from this map. @@ -371,7 +375,7 @@ public class GroupPane extends BorderPane { case FIVE: return cat5Toggle; default: - throw new IllegalArgumentException(category.name()); + throw new UnsupportedOperationException("Unknown category: " + category.name()); } } @@ -425,51 +429,41 @@ public class GroupPane extends BorderPane { DoubleBinding cellSize = controller.thumbnailSizeProperty().add(75); gridView.cellHeightProperty().bind(cellSize); gridView.cellWidthProperty().bind(cellSize); - gridView.setCellFactory((GridView param) -> new DrawableCell()); + gridView.setCellFactory(param -> new DrawableCell()); BooleanBinding isSelectionEmpty = Bindings.isEmpty(selectionModel.getSelected()); catSelectedSplitMenu.disableProperty().bind(isSelectionEmpty); tagSelectedSplitMenu.disableProperty().bind(isSelectionEmpty); - TagSelectedFilesAction followUpSelectedAction = new TagSelectedFilesAction(controller.getTagsManager().getFollowUpTagName(), controller); //NON-NLS - Platform.runLater(() -> { - tagSelectedSplitMenu.setText(followUpSelectedAction.getText()); - tagSelectedSplitMenu.setGraphic(followUpSelectedAction.getGraphic()); - tagSelectedSplitMenu.setOnAction(followUpSelectedAction); - tagSelectedSplitMenu.showingProperty().addListener(showing -> { - if (tagSelectedSplitMenu.isShowing()) { - - ListenableFuture> getTagsFuture = exec.submit(() - -> Lists.transform(controller.getTagsManager().getNonCategoryTagNames(), - tagName -> GuiUtils.createAutoAssigningMenuItem(tagSelectedSplitMenu, new TagSelectedFilesAction(tagName, controller)))); - Futures.addCallback(getTagsFuture, new FutureCallback>() { - @Override - public void onSuccess(List result) { - tagSelectedSplitMenu.getItems().setAll(result); - } - - @Override - public void onFailure(Throwable throwable) { - logger.log(Level.SEVERE, "Error getting tag names.", throwable); - } - }, Platform::runLater); - } - }); - }); + addFXCallback(exec.submit(() -> controller.getTagsManager().getFollowUpTagName()), + followUpTagName -> { + //on fx thread + TagSelectedFilesAction followUpSelectedAction = new TagSelectedFilesAction(followUpTagName, controller); + tagSelectedSplitMenu.setText(followUpSelectedAction.getText()); + tagSelectedSplitMenu.setGraphic(followUpSelectedAction.getGraphic()); + tagSelectedSplitMenu.setOnAction(followUpSelectedAction); + }, + throwable -> logger.log(Level.SEVERE, "Error getting tag names.", throwable)); + addFXCallback(exec.submit(() -> controller.getTagsManager().getNonCategoryTagNames()), + tagNames -> { + //on fx thread + List menuItems = transform(tagNames, + tagName -> createAutoAssigningMenuItem(tagSelectedSplitMenu, new TagSelectedFilesAction(tagName, controller))); + tagSelectedSplitMenu.getItems().setAll(menuItems); + }, + throwable -> logger.log(Level.SEVERE, "Error getting tag names.", throwable)//NON-NLS + ); CategorizeSelectedFilesAction cat5SelectedAction = new CategorizeSelectedFilesAction(DhsImageCategory.FIVE, controller); catSelectedSplitMenu.setOnAction(cat5SelectedAction); catSelectedSplitMenu.setText(cat5SelectedAction.getText()); catSelectedSplitMenu.setGraphic(cat5SelectedAction.getGraphic()); - catSelectedSplitMenu.showingProperty().addListener(showing -> { - if (catSelectedSplitMenu.isShowing()) { - List categoryMenues = Lists.transform(Arrays.asList(DhsImageCategory.values()), - cat -> GuiUtils.createAutoAssigningMenuItem(catSelectedSplitMenu, new CategorizeSelectedFilesAction(cat, controller))); - catSelectedSplitMenu.getItems().setAll(categoryMenues); - } - }); + + List categoryMenues = transform(asList(DhsImageCategory.values()), + cat -> createAutoAssigningMenuItem(catSelectedSplitMenu, new CategorizeSelectedFilesAction(cat, controller))); + catSelectedSplitMenu.getItems().setAll(categoryMenues); slideShowToggle.getStyleClass().remove("radio-button"); slideShowToggle.getStyleClass().add("toggle-button"); @@ -675,7 +669,6 @@ public class GroupPane extends BorderPane { } void makeSelection(Boolean shiftDown, Long newFileID) { - if (shiftDown) { //TODO: do more hear to implement slicker multiselect int endIndex = grouping.get().getFileIDs().indexOf(newFileID); @@ -688,7 +681,6 @@ public class GroupPane extends BorderPane { } else { selectionAnchorIndex = null; selectionModel.clearAndSelect(newFileID); - } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/MetaDataPane.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/MetaDataPane.java index 645c67baba..7766317995 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/MetaDataPane.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/MetaDataPane.java @@ -22,6 +22,7 @@ import com.google.common.eventbus.Subscribe; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import static java.util.Collections.singletonMap; import java.util.List; import java.util.Objects; import static java.util.Objects.isNull; @@ -112,9 +113,7 @@ public class MetaDataPane extends DrawableUIBase { }); copyMenuItem.setAccelerator(COPY_KEY_COMBINATION); - copyMenuItem.setOnAction(actionEvent -> { - copyValueToClipBoard(); - }); + copyMenuItem.setOnAction(actionEvent -> copyValueToClipBoard()); tableView.setContextMenu(contextMenu); tableView.setOnKeyPressed((KeyEvent event) -> { @@ -220,9 +219,6 @@ public class MetaDataPane extends DrawableUIBase { return imageBorder; } - /** - * {@inheritDoc } - */ @Subscribe @Override public void handleCategoryChanged(CategoryManager.CategoryChangeEvent evt) { @@ -256,9 +252,9 @@ public class MetaDataPane extends DrawableUIBase { private void copyValueToClipBoard() { Pair, Collection> selectedItem = tableView.getSelectionModel().getSelectedItem(); if (nonNull(selectedItem)) { - Clipboard.getSystemClipboard().setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, - getValueDisplayString(selectedItem))); + Clipboard.getSystemClipboard().setContent( + singletonMap(DataFormat.PLAIN_TEXT, getValueDisplayString(selectedItem)) + ); } } - } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/SlideShowView.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/SlideShowView.java index 11d109bbb5..e4d565d0ee 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/SlideShowView.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/SlideShowView.java @@ -131,8 +131,8 @@ public class SlideShowView extends DrawableTileBase { getGroupPane().grouping().addListener(observable -> { syncButtonVisibility(); if (getGroupPane().getGroup() != null) { - getGroupPane().getGroup().getFileIDs().addListener((Observable observable1) -> - syncButtonVisibility()); + getGroupPane().getGroup().getFileIDs().addListener((Observable observable1) + -> syncButtonVisibility()); } }); } @@ -215,9 +215,7 @@ public class SlideShowView extends DrawableTileBase { mediaTask = null; } }); - myTask.setOnCancelled(cancelled -> { - disposeContent(); - }); + myTask.setOnCancelled(cancelled -> disposeContent()); exec.execute(myTask); return progressNode; @@ -245,7 +243,6 @@ public class SlideShowView extends DrawableTileBase { /** * - * @param file the value of file * @param imageTask the value of imageTask */ @Override @@ -259,9 +256,6 @@ public class SlideShowView extends DrawableTileBase { return maskerPane; } - /** - * {@inheritDoc } - */ @Override protected String getTextForLabel() { return getFile().map(DrawableFile::getName).orElse("") + " " + getSupplementalText(); @@ -292,8 +286,8 @@ public class SlideShowView extends DrawableTileBase { * of y" */ @NbBundle.Messages({"# {0} - file id number", - "# {1} - number of file ids", - "SlideShowView.supplementalText={0} of {1} in group"}) + "# {1} - number of file ids", + "SlideShowView.supplementalText={0} of {1} in group"}) private String getSupplementalText() { final ObservableList fileIds = getGroupPane().getGroup().getFileIDs(); return getFileID().map(fileID -> " ( " + Bundle.SlideShowView_supplementalText(fileIds.indexOf(fileID) + 1, fileIds.size()) + " )") @@ -301,9 +295,6 @@ public class SlideShowView extends DrawableTileBase { } - /** - * {@inheritDoc } - */ @Override @ThreadConfined(type = ThreadType.ANY) public DhsImageCategory updateCategory() { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/HashHitGroupList.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/HashHitGroupList.java index 3799f943ef..c7f9fb6ed4 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/HashHitGroupList.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/navpanel/HashHitGroupList.java @@ -77,7 +77,7 @@ final public class HashHitGroupList extends NavPanel { setGraphic(new ImageView("org/sleuthkit/autopsy/imagegallery/images/hashset_hits.png")); //NON-NLS getBorderPane().setCenter(groupList); - sorted = getController().getGroupManager().getAnalyzedGroups().filtered((DrawableGroup t) -> t.getHashSetHitsCount() > 0).sorted(getDefaultComparator()); + sorted = getController().getGroupManager().getAnalyzedGroups().filtered(group -> group.getHashSetHitsCount() > 0).sorted(getDefaultComparator()); GroupCellFactory groupCellFactory = new GroupCellFactory(getController(), comparatorProperty()); groupList.setCellFactory(groupCellFactory::getListCell); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/utils/TaskUtils.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/utils/TaskUtils.java index 9c26b9788c..61fdcb4e1b 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/utils/TaskUtils.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/utils/TaskUtils.java @@ -18,11 +18,16 @@ */ package org.sleuthkit.autopsy.imagegallery.utils; +import com.google.common.util.concurrent.FutureCallback; +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 com.google.common.util.concurrent.ThreadFactoryBuilder; import java.util.concurrent.Callable; import java.util.concurrent.Executors; +import java.util.function.Consumer; +import javafx.application.Platform; import javafx.concurrent.Task; /** @@ -42,8 +47,26 @@ public final class TaskUtils { }; } - public static ListeningExecutorService getExecutorForClass(Class clazz) { + public static ListeningExecutorService getExecutorForClass(Class clazz) { return MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor( new ThreadFactoryBuilder().setNameFormat("Image Gallery " + clazz.getSimpleName() + " BG Thread").build())); } + + public static void addFXCallback(ListenableFuture future, Consumer onSuccess, Consumer onFailure) { + Futures.addCallback(future, makeFutureCallBack(onSuccess, onFailure), Platform::runLater); + } + + public static FutureCallback< X> makeFutureCallBack(Consumer onSuccess, Consumer onFailure) { + return new FutureCallback() { + @Override + public void onSuccess(X result) { + onSuccess.accept(result); + } + + @Override + public void onFailure(Throwable t) { + onFailure.accept(t); + } + }; + } } From 2bbb7a9c4f35e106adaff44be882e58d26d3866a Mon Sep 17 00:00:00 2001 From: millmanorama Date: Fri, 5 Oct 2018 00:17:55 +0200 Subject: [PATCH 2/4] show dialog describing behaviour of multi user cases. --- .../imagegallery/ImageGalleryPreferences.java | 20 +- .../imagegallery/actions/OpenAction.java | 172 ++++++++++++------ .../autopsy/imagegallery/utils/TaskUtils.java | 25 ++- 3 files changed, 160 insertions(+), 57 deletions(-) diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryPreferences.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryPreferences.java index 37be779969..cb912c2a79 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryPreferences.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryPreferences.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-16 Basis Technology Corp. + * Copyright 2013-18 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,6 +37,7 @@ public class ImageGalleryPreferences { */ private static final String ENABLED_BY_DEFAULT = "enabled_by_default"; //NON-NLS private static final String GROUP_CATEGORIZATION_WARNING_DISABLED = "group_categorization_warning_disabled"; //NON-NLS + private static final String MULTI_USER_CASE_INFO_DIALOG_DISABLED = "multi_user_case_info_dialog_disabled"; //NON-NLS /** * Return setting of whether Image Analyzer should be automatically enabled @@ -46,7 +47,7 @@ public class ImageGalleryPreferences { * @return true if new cases should have image analyzer enabled. */ public static boolean isEnabledByDefault() { - return preferences.getBoolean(ENABLED_BY_DEFAULT, true); + return preferences.getBoolean(ENABLED_BY_DEFAULT, true); } public static void setEnabledByDefault(boolean b) { @@ -68,6 +69,21 @@ public class ImageGalleryPreferences { preferences.putBoolean(GROUP_CATEGORIZATION_WARNING_DISABLED, b); } + /** + * Return whether the dialog describing multi user case updating is + * disabled. + * + * @return true if the dialog is disabled. + */ + public static boolean isMultiUserCaseInfoDialogDisabled() { + final boolean aBoolean = preferences.getBoolean(MULTI_USER_CASE_INFO_DIALOG_DISABLED, false); + return aBoolean; + } + + public static void setMultiUserCaseInfoDialogDisabled(boolean b) { + preferences.putBoolean(MULTI_USER_CASE_INFO_DIALOG_DISABLED, b); + } + static void addChangeListener(PreferenceChangeListener l) { preferences.addPreferenceChangeListener(l); } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java index 3794c69e83..58d4e3c5d9 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java @@ -18,14 +18,25 @@ */ package org.sleuthkit.autopsy.imagegallery.actions; +import com.google.common.util.concurrent.ListenableFuture; +import static com.google.common.util.concurrent.MoreExecutors.listeningDecorator; import java.awt.Component; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import static java.util.concurrent.Executors.newSingleThreadExecutor; import java.util.logging.Level; +import javafx.application.Platform; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Label; +import javafx.scene.layout.VBox; +import javafx.stage.Modality; +import javax.annotation.concurrent.ThreadSafe; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JMenuItem; -import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; import org.openide.awt.ActionID; import org.openide.awt.ActionReference; import org.openide.awt.ActionReferences; @@ -34,15 +45,18 @@ import org.openide.util.HelpCtx; import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; import org.openide.util.actions.CallableSystemAction; -import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.core.Installer; import org.sleuthkit.autopsy.core.RuntimeProperties; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.ImageGalleryModule; +import org.sleuthkit.autopsy.imagegallery.ImageGalleryPreferences; import org.sleuthkit.autopsy.imagegallery.ImageGalleryTopComponent; +import org.sleuthkit.autopsy.imagegallery.gui.GuiUtils; +import static org.sleuthkit.autopsy.imagegallery.utils.TaskUtils.addFXCallback; import org.sleuthkit.datamodel.TskCoreException; @ActionID(category = "Tools", id = "org.sleuthkit.autopsy.imagegallery.OpenAction") @@ -119,9 +133,13 @@ public final class OpenAction extends CallableSystemAction { } @Override - @NbBundle.Messages({"OpenAction.dialogTitle=Image Gallery"}) + @NbBundle.Messages({"OpenAction.dialogTitle=Image Gallery", + "OpenAction.multiUserDialog.Header=Multi-user Image Gallery", + "OpenAction.multiUserDialog.ContentText=The Image Gallery updates itself differently for multi-user cases than single user cases. Notably:\n\n" + + "If your computer is analyzing a data source, then you will get real-time Image Gallery updates as files are analyzed (hashed, EXIF, etc.). This is the same behavior as a single-user case.\n\n" + + "If another computer in your multi-user cluster is analyzing a data source, you will get updates about files on that data source only when you launch Image Gallery, which will cause the local database to be rebuilt based on results from other nodes.", + "OpenAction.multiUserDialog.checkBox.text=Don't show this message again."}) public void performAction() { - //check case final Case currentCase; try { @@ -130,57 +148,103 @@ public final class OpenAction extends CallableSystemAction { logger.log(Level.SEVERE, "Exception while getting open case.", ex); return; } - + ImageGalleryController controller; try { - ImageGalleryController controller = ImageGalleryModule.getController(); - if (controller.isDataSourcesTableStale()) { - //drawable db is stale, ask what to do - int answer = JOptionPane.showConfirmDialog( - WindowManager.getDefault().getMainWindow(), - Bundle.OpenAction_stale_confDlg_msg(), - Bundle.OpenAction_stale_confDlg_title(), - JOptionPane.YES_NO_CANCEL_OPTION, - JOptionPane.WARNING_MESSAGE); - - switch (answer) { - case JOptionPane.YES_OPTION: - /* For a single-user case, we favor user experience, and - * rebuild the database as soon as Image Gallery is - * enabled for the case. For a multi-user case, we favor - * overall performance and user experience, not every - * user may want to review images, so we rebuild the - * database only when a user launches Image Gallery. - */ - if (currentCase.getCaseType() == Case.CaseType.SINGLE_USER_CASE) { - /* - * Turning listening off is necessary in order to - * invoke the listener that will call - * controller.rebuildDB(); - */ - controller.setListeningEnabled(false); - controller.setListeningEnabled(true); - } else { - controller.rebuildDB(); - } - ImageGalleryTopComponent.openTopComponent(); - break; - - case JOptionPane.NO_OPTION: { - ImageGalleryTopComponent.openTopComponent(); - } - break; - case JOptionPane.CANCEL_OPTION: - break; //do nothing - } - } else { - //drawable db is not stale, just open it - ImageGalleryTopComponent.openTopComponent(); - } - } catch (NoCurrentCaseException ex) { - logger.log(Level.SEVERE, "Attempted to access ImageGallery with no case open.", ex); //NON-NLS - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error getting ImageGalleryController.", ex); //NON-NLS + controller = ImageGalleryModule.getController(); + } catch (TskCoreException | NoCurrentCaseException ex) { + logger.log(Level.SEVERE, "Exception while getting ImageGalleryController for current case.", ex); + return; } + Platform.runLater(() -> { + if (currentCase.getCaseType() == Case.CaseType.MULTI_USER_CASE + && ImageGalleryPreferences.isMultiUserCaseInfoDialogDisabled() == false) { + Alert dialog = new Alert(Alert.AlertType.INFORMATION); + dialog.initModality(Modality.APPLICATION_MODAL); + dialog.setResizable(true); + dialog.setTitle(Bundle.OpenAction_dialogTitle()); + dialog.setHeaderText(Bundle.OpenAction_multiUserDialog_Header()); + + Label label = new Label(Bundle.OpenAction_multiUserDialog_ContentText()); + label.setMaxWidth(450); + label.setWrapText(true); + CheckBox dontShowAgainCheckBox = new CheckBox(Bundle.OpenAction_multiUserDialog_checkBox_text()); + dialog.getDialogPane().setContent(new VBox(10, label, dontShowAgainCheckBox)); + GuiUtils.setDialogIcons(dialog); + + dialog.showAndWait(); + + if (dialog.getResult() == ButtonType.OK && dontShowAgainCheckBox.isSelected()) { + ImageGalleryPreferences.setMultiUserCaseInfoDialogDisabled(true); + } + } + + checkDBStale(controller); + }); + } + + private void checkDBStale(ImageGalleryController controller) { + //check if db is stale on throw away bg thread and then react back on jfx thread. + ListenableFuture staleFuture = listeningDecorator(newSingleThreadExecutor()) + .submit(controller::isDataSourcesTableStale); + addFXCallback(staleFuture, + dbIsStale -> { + //back on fx thread. + if (false == dbIsStale) { + //drawable db is not stale, just open it + openTopComponent(); + } else { + //drawable db is stale, ask what to do + Alert alert = new Alert(Alert.AlertType.WARNING, + Bundle.OpenAction_stale_confDlg_msg(), + ButtonType.YES, ButtonType.NO, ButtonType.CANCEL); + alert.initModality(Modality.APPLICATION_MODAL); + alert.setTitle(Bundle.OpenAction_stale_confDlg_title()); + GuiUtils.setDialogIcons(alert); + ButtonType answer = alert.showAndWait().orElse(ButtonType.CANCEL); + if (answer == ButtonType.CANCEL) { + //just do nothing + } else if (answer == ButtonType.NO) { + openTopComponent(); + } else if (answer == ButtonType.YES) { + if (controller.getAutopsyCase().getCaseType() == Case.CaseType.SINGLE_USER_CASE) { + /* For a single-user case, we favor user + * experience, and rebuild the database as soon + * as Image Gallery is enabled for the case. + * + * Turning listening off is necessary in order + * to invoke the listener that will call + * controller.rebuildDB(); + */ + controller.setListeningEnabled(false); + controller.setListeningEnabled(true); + } else { + /* + * For a multi-user case, we favor overall + * performance and user experience, not every + * user may want to review images, so we rebuild + * the database only when a user launches Image + * Gallery. + */ + controller.rebuildDB(); + } + openTopComponent(); + } + } + }, + throwable -> logger.log(Level.SEVERE, "Error checking if drawable db is stale.", throwable)//NON-NLS + ); + } + + private void openTopComponent() { + SwingUtilities.invokeLater(() -> { + try { + ImageGalleryTopComponent.openTopComponent(); + } catch (NoCurrentCaseException ex) { + logger.log(Level.SEVERE, "Attempted to access ImageGallery with no case open.", ex);//NON-NLS + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error getting ImageGalleryController.", ex); //NON-NLS} + } + }); } @Override @@ -195,6 +259,6 @@ public final class OpenAction extends CallableSystemAction { @Override public boolean asynchronous() { - return false; // run on edt + return true; // run off edt } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/utils/TaskUtils.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/utils/TaskUtils.java index 9c26b9788c..61fdcb4e1b 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/utils/TaskUtils.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/utils/TaskUtils.java @@ -18,11 +18,16 @@ */ package org.sleuthkit.autopsy.imagegallery.utils; +import com.google.common.util.concurrent.FutureCallback; +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 com.google.common.util.concurrent.ThreadFactoryBuilder; import java.util.concurrent.Callable; import java.util.concurrent.Executors; +import java.util.function.Consumer; +import javafx.application.Platform; import javafx.concurrent.Task; /** @@ -42,8 +47,26 @@ public final class TaskUtils { }; } - public static ListeningExecutorService getExecutorForClass(Class clazz) { + public static ListeningExecutorService getExecutorForClass(Class clazz) { return MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor( new ThreadFactoryBuilder().setNameFormat("Image Gallery " + clazz.getSimpleName() + " BG Thread").build())); } + + public static void addFXCallback(ListenableFuture future, Consumer onSuccess, Consumer onFailure) { + Futures.addCallback(future, makeFutureCallBack(onSuccess, onFailure), Platform::runLater); + } + + public static FutureCallback< X> makeFutureCallBack(Consumer onSuccess, Consumer onFailure) { + return new FutureCallback() { + @Override + public void onSuccess(X result) { + onSuccess.accept(result); + } + + @Override + public void onFailure(Throwable t) { + onFailure.accept(t); + } + }; + } } From a2998329db74e13e159e4e06fe4eee33d45f7ebc Mon Sep 17 00:00:00 2001 From: millmanorama Date: Fri, 5 Oct 2018 00:21:24 +0200 Subject: [PATCH 3/4] use getExecutorForClass() --- .../org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java index 58d4e3c5d9..81431b771e 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java @@ -56,6 +56,7 @@ import org.sleuthkit.autopsy.imagegallery.ImageGalleryModule; import org.sleuthkit.autopsy.imagegallery.ImageGalleryPreferences; import org.sleuthkit.autopsy.imagegallery.ImageGalleryTopComponent; import org.sleuthkit.autopsy.imagegallery.gui.GuiUtils; +import org.sleuthkit.autopsy.imagegallery.utils.TaskUtils; import static org.sleuthkit.autopsy.imagegallery.utils.TaskUtils.addFXCallback; import org.sleuthkit.datamodel.TskCoreException; @@ -184,7 +185,7 @@ public final class OpenAction extends CallableSystemAction { private void checkDBStale(ImageGalleryController controller) { //check if db is stale on throw away bg thread and then react back on jfx thread. - ListenableFuture staleFuture = listeningDecorator(newSingleThreadExecutor()) + ListenableFuture staleFuture = TaskUtils.getExecutorForClass(OpenAction.class) .submit(controller::isDataSourcesTableStale); addFXCallback(staleFuture, dbIsStale -> { From 2080ae357d9a6ebff83a87e1deb1b3810da3de9e Mon Sep 17 00:00:00 2001 From: Brian Carrier Date: Fri, 5 Oct 2018 16:38:37 -0400 Subject: [PATCH 4/4] update equals to equivalent to hashCode and use only DS ID for comparision instead of needting to call getChildren() --- .../autopsy/imagegallery/datamodel/grouping/GroupKey.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupKey.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupKey.java index 00d3c3d728..8b1f6eb31a 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupKey.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupKey.java @@ -99,7 +99,7 @@ public class GroupKey> implements Comparable if (!Objects.equals(this.attr, other.attr)) { return false; } - return Objects.equals(this.dataSource, other.dataSource); + return this.dataSource.getId() == other.dataSource.getId(); } @Override