/* * Autopsy Forensic Browser * * Copyright 2013-2019 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; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.logging.Level; import java.util.stream.Collectors; import javafx.application.Platform; import javafx.beans.Observable; import javafx.concurrent.Task; import javafx.embed.swing.JFXPanel; import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; import javafx.scene.control.ChoiceDialog; import javafx.scene.control.ComboBox; import javafx.scene.control.ListView; import javafx.scene.control.ProgressIndicator; import javafx.scene.control.SplitPane; import javafx.scene.control.TabPane; import javafx.scene.layout.Background; import javafx.scene.layout.BackgroundFill; import javafx.scene.layout.BorderPane; import javafx.scene.layout.CornerRadii; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.stage.Modality; import javax.annotation.concurrent.GuardedBy; import javax.swing.JComponent; import javax.swing.SwingUtilities; import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; import static org.apache.commons.lang3.ObjectUtils.notEqual; import org.openide.explorer.ExplorerManager; import org.openide.explorer.ExplorerUtils; import org.openide.util.Lookup; import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; import org.openide.windows.Mode; import org.openide.windows.RetainLocation; import org.openide.windows.TopComponent; import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.directorytree.ExternalViewerShortcutAction; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute; import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupManager; import org.sleuthkit.autopsy.imagegallery.gui.DataSourceCell; import org.sleuthkit.autopsy.imagegallery.gui.GuiUtils; import org.sleuthkit.autopsy.imagegallery.gui.NoGroupsDialog; import org.sleuthkit.autopsy.imagegallery.gui.StatusBar; import org.sleuthkit.autopsy.imagegallery.gui.SummaryTablePane; import org.sleuthkit.autopsy.imagegallery.gui.Toolbar; import org.sleuthkit.autopsy.imagegallery.gui.drawableviews.GroupPane; import org.sleuthkit.autopsy.imagegallery.gui.drawableviews.MetaDataPane; import org.sleuthkit.autopsy.imagegallery.gui.navpanel.GroupTree; import org.sleuthkit.autopsy.imagegallery.gui.navpanel.HashHitGroupList; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.datamodel.DataSource; import org.sleuthkit.datamodel.TskCoreException; /** * The singleton Image Gallery top component. */ @TopComponent.Description( preferredID = "ImageGalleryTopComponent", //iconBase = "org/sleuthkit/autopsy/imagegallery/images/lightbulb.png", /*use this to put icon in window title area*/ persistenceType = TopComponent.PERSISTENCE_NEVER) @RetainLocation("ImageGallery") @TopComponent.Registration(mode = "ImageGallery", openAtStartup = false) @Messages({ "CTL_ImageGalleryAction=Image/Video Gallery", "CTL_ImageGalleryTopComponent=Image/Video Gallery" }) @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives public final class ImageGalleryTopComponent extends TopComponent implements ExplorerManager.Provider, Lookup.Provider { private static final long serialVersionUID = 1L; public final static String PREFERRED_ID = "ImageGalleryTopComponent"; // NON-NLS // RJCTODO: This should not be public, clients should call getTopComponent instead private static final Logger logger = Logger.getLogger(ImageGalleryTopComponent.class.getName()); private final ExplorerManager em = new ExplorerManager(); private final Lookup lookup = (ExplorerUtils.createLookup(em, getActionMap())); private final Object controllerLock = new Object(); @GuardedBy("controllerLock") private ImageGalleryController controller; private SplitPane splitPane; private StackPane centralStack; private final BorderPane borderPane = new BorderPane(); private StackPane fullUIStack; private MetaDataPane metaDataTable; private GroupPane groupPane; private GroupTree groupTree; private HashHitGroupList hashHitList; private VBox leftPane; private Scene myScene; private Node infoOverlay; private final Region infoOverLayBackground = new TranslucentRegion(); /** * Queries whether the singleton Image Gallery top component's window is * open. Note that calling this method will cause the top component to be * constructed if it does not already exist. * * @return True or false. */ public static boolean isImageGalleryOpen() { return getTopComponent().isOpened(); } /** * Gets the singleton Image Gallery top component. Note that calling this * method will cause the top component to be constructed if it does not * already exist. * * @return The top component. */ public static ImageGalleryTopComponent getTopComponent() { return (ImageGalleryTopComponent) WindowManager.getDefault().findTopComponent(PREFERRED_ID); } /** * Creates the Image Gallery top component if it does not already exist and * opens its window. * * @throws TskCoreException If there is a problem opening the top component. */ @ThreadConfined(type = ThreadConfined.ThreadType.AWT) public static void openTopComponent() throws TskCoreException { final ImageGalleryTopComponent topComponent = getTopComponent(); if (topComponent.isOpened()) { showTopComponent(); } else { topComponent.getCurrentControllerAndOpen(); } } /** * Configures the groups manager for the selected data source(s) and opens * this top component's window. * * @param selectedDataSource The data source selected, null if all data * sources are selected. * @param dataSourcesTooManyFiles A map of data sources to flags indicating * whether or not the data source has to many * files to actually be displayed. */ private void openWithSelectedDataSources(DataSource selectedDataSource, Map dataSourcesTooManyFiles) { if (dataSourcesTooManyFiles.get(selectedDataSource)) { Platform.runLater(ImageGalleryTopComponent::showTooManyFiles); } else { SwingUtilities.invokeLater(() -> showTopComponent()); synchronized (controllerLock) { GroupManager groupManager = controller.getGroupManager(); // RJCTODO: Why are there potentially hazardous nested synchronized // blocks here (note: method used to be synchronized, my // dedicated controllerLock lock just makes the nesting more obvious)? // Why is the groups manager not taking responsibility for its own thread // safety policy? synchronized (groupManager) { groupManager.regroup(selectedDataSource, groupManager.getGroupBy(), groupManager.getSortBy(), groupManager.getSortOrder(), true); } } } } /** * Displays a dialog box informing the user that the data source(s) selected * to have their images displayed have too many image files and will not be * displayed. */ @NbBundle.Messages({"ImageGallery.dialogTitle=Image Gallery", "ImageGallery.showTooManyFiles.contentText=There are too many files in the selected datasource(s) to ensure reasonable performance.", "ImageGallery.showTooManyFiles.headerText="}) private static void showTooManyFiles() { Alert dialog = new Alert(Alert.AlertType.INFORMATION, Bundle.ImageGallery_showTooManyFiles_contentText(), ButtonType.OK); dialog.initModality(Modality.APPLICATION_MODAL); dialog.setTitle(Bundle.ImageGallery_dialogTitle()); GuiUtils.setDialogIcons(dialog); dialog.setHeaderText(Bundle.ImageGallery_showTooManyFiles_headerText()); dialog.showAndWait(); } /** * Opens the singleton top component's window, brings it to the front and * gives it focus. Note that calling this method will cause the top * component to be constructed if it does not already exist. */ @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private static void showTopComponent() { final ImageGalleryTopComponent topComponent = getTopComponent(); if (topComponent.isOpened() == false) { topComponent.open(); } topComponent.toFront(); topComponent.requestActive(); } /* * Closes the singleton Image Gallery top component. Note that calling this * method will cause the top component to be constructed if it does not * already exist. */ public static void closeTopComponent() { // RJCTODO: Could add the flag that used to be used for the busy wait on // the initial JavaFX thread task to avoid superfluous construction here. getTopComponent().close(); } /** * Contructs the singleton Image Gallery top component. Called by the * NetBeans WindowManager. */ public ImageGalleryTopComponent() { setName(Bundle.CTL_ImageGalleryTopComponent()); initComponents(); getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(ExternalViewerShortcutAction.EXTERNAL_VIEWER_SHORTCUT, "useExternalViewer"); //NON-NLS getActionMap().put("useExternalViewer", ExternalViewerShortcutAction.getInstance()); //NON-NLS } /** * Gets the current controller, allows the user to select the data sources * for which images are to be displayed and opens the top component's * window. * * @throws TskCoreException If there is an error getting the current * controller. */ @Messages({ "ImageGalleryTopComponent.chooseDataSourceDialog.headerText=Choose a data source to view.", "ImageGalleryTopComponent.chooseDataSourceDialog.contentText=Data source:", "ImageGalleryTopComponent.chooseDataSourceDialog.all=All", "ImageGalleryTopComponent.chooseDataSourceDialog.titleText=Image Gallery",}) private void getCurrentControllerAndOpen() throws TskCoreException { ImageGalleryController currentController = ImageGalleryModule.getController(); /* * Dispatch a task to run in the JavaFX thread. This task will swap the * new controller, if there is one, into this top component and its * child UI components. This task also queues another JavaFX thread task * to check for analyzed groups, which has the side effect of starting * the spinner(s) that take the place of a wait cursor. Finally, this * task starts a background thread to query the case database. This * background task may dispatch a JavaFX thread task to do a data source * selection dialog. Ultimately, there is a final task that either opens * the window in the AWT EDT or displays a "too many files" dialog in * the JFX thread. */ // RJCTODO: Verify the side effect remark above. Platform.runLater(new Runnable() { @Override public void run() { synchronized (controllerLock) { if (notEqual(controller, currentController)) { controller = currentController; /* * Create or re-create the top component's child UI * components. This is currently done every time a new * controller is created (i.e., a new case is opened). * It could be done by resetting the controller in the * child UI components instead. */ // RJCTODO: Construction of these components can perhaps // be separated from opening the window again so that // a setController implementation could be called from // the case opened event handler in the ImageGalleryModule // object. fullUIStack = new StackPane(); myScene = new Scene(fullUIStack); jfxPanel.setScene(myScene); groupPane = new GroupPane(controller); centralStack = new StackPane(groupPane); fullUIStack.getChildren().add(borderPane); splitPane = new SplitPane(); borderPane.setCenter(splitPane); Toolbar toolbar = new Toolbar(controller); borderPane.setTop(toolbar); borderPane.setBottom(new StatusBar(controller)); metaDataTable = new MetaDataPane(controller); groupTree = new GroupTree(controller); hashHitList = new HashHitGroupList(controller); TabPane tabPane = new TabPane(groupTree, hashHitList); tabPane.setPrefWidth(TabPane.USE_COMPUTED_SIZE); tabPane.setMinWidth(TabPane.USE_PREF_SIZE); VBox.setVgrow(tabPane, Priority.ALWAYS); leftPane = new VBox(tabPane, new SummaryTablePane(controller)); SplitPane.setResizableWithParent(leftPane, Boolean.FALSE); SplitPane.setResizableWithParent(groupPane, Boolean.TRUE); SplitPane.setResizableWithParent(metaDataTable, Boolean.FALSE); splitPane.getItems().addAll(leftPane, centralStack, metaDataTable); splitPane.setDividerPositions(0.1, 1.0); /* * Set up for a call to checkForGroups to happen * whenever the controller's regrouping disabled * property or the group manager's analyzed groups * property changes. */ controller.regroupDisabledProperty().addListener((Observable unused) -> Platform.runLater(() -> checkForAnalyzedGroups())); controller.getGroupManager().getAnalyzedGroups().addListener((Observable unused) -> Platform.runLater(() -> checkForAnalyzedGroups())); /* * Dispatch a later task to call check for groups. Note * that this method displays one or more spinner(s) that * take the place of a wait cursor if there are no * analyzed groups yet, ingest is running, etc. */ // RJCTODO: Is there a race condition here, since this task could be // executed before the task to actually open the top component window? // It seems like this might be a sort of a hack and I am wondering // why this can't be done in openWithSelectedDataSources instead. Platform.runLater(() -> checkForAnalyzedGroups()); } /* * Kick off a background task to query the case database for * data sources. This task may queue another task for the * JavaFX thread to allow the user to select which data * sources for which to display images. Ultimately, a task * will be queued for the AWT EDT that will show the top * component window. */ new Thread(new Task() { @Override protected Void call() throws Exception { synchronized (controllerLock) { /* * If there is only one datasource or the * grouping criterion is already set to * something other than by path (the default), * proceed to open this top component. * Otherwise, do a dialog to allow the user to * select the data sources for which images are * to be displayed, then open the top component. */ List dataSources = controller.getSleuthKitCase().getDataSources(); Map dataSourcesWithTooManyFiles = new HashMap<>(); // RJCTODO: At least some of this designation of "all data sources" with null seems uneccessary; // in any case, the use of nulls and zeros here is // very confusing and should be reworked. if (dataSources.size() <= 1 || controller.getGroupManager().getGroupBy() != DrawableAttribute.PATH) { dataSourcesWithTooManyFiles.put(null, controller.hasTooManyFiles(null)); openWithSelectedDataSources(null, dataSourcesWithTooManyFiles); } else { dataSources.add(0, null); for (DataSource dataSource : dataSources) { dataSourcesWithTooManyFiles.put(dataSource, controller.hasTooManyFiles(dataSource)); } Platform.runLater(() -> { List> dataSourceOptionals = dataSources.stream().map(Optional::ofNullable).collect(Collectors.toList()); ChoiceDialog> datasourceDialog = new ChoiceDialog<>(null, dataSourceOptionals); datasourceDialog.setTitle(Bundle.ImageGalleryTopComponent_chooseDataSourceDialog_titleText()); datasourceDialog.setHeaderText(Bundle.ImageGalleryTopComponent_chooseDataSourceDialog_headerText()); datasourceDialog.setContentText(Bundle.ImageGalleryTopComponent_chooseDataSourceDialog_contentText()); datasourceDialog.initModality(Modality.APPLICATION_MODAL); GuiUtils.setDialogIcons(datasourceDialog); @SuppressWarnings(value = "unchecked") ComboBox> comboBox = (ComboBox>) datasourceDialog.getDialogPane().lookup(".combo-box"); comboBox.setCellFactory((ListView> unused) -> new DataSourceCell(dataSourcesWithTooManyFiles, controller.getAllDataSourcesDrawableDBStatus())); comboBox.setButtonCell(new DataSourceCell(dataSourcesWithTooManyFiles, controller.getAllDataSourcesDrawableDBStatus())); DataSource dataSource = datasourceDialog.showAndWait().orElse(Optional.empty()).orElse(null); openWithSelectedDataSources(dataSource, dataSourcesWithTooManyFiles); }); } return null; } } }).start(); } } }); } /** * This method is called from within the constructor to initialize the form. * WARNING: Do NOT modify this code. The content of this method is always * regenerated by the Form Editor. */ // //GEN-BEGIN:initComponents private void initComponents() { jfxPanel = new JFXPanel(); javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(jfxPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 532, Short.MAX_VALUE) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(jfxPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 389, Short.MAX_VALUE) ); }// //GEN-END:initComponents // Variables declaration - do not modify//GEN-BEGIN:variables private javafx.embed.swing.JFXPanel jfxPanel; // End of variables declaration//GEN-END:variables @Override public List availableModes(List modes) { /* * This looks like the right thing to do, but online discussions seems * to indicate this method is effectively deprecated. A break point * placed here was never hit. */ return modes.stream().filter(mode -> mode.getName().equals("timeline") || mode.getName().equals("ImageGallery")) .collect(Collectors.toList()); } @Override public void componentOpened() { super.componentOpened(); WindowManager.getDefault().setTopComponentFloating(this, true); } @Override public ExplorerManager getExplorerManager() { return em; } @Override public Lookup getLookup() { return lookup; } /** * Checks if there are any fully analyzed groups available from the groups * manager and removes the blocking progress spinner if there are analyzed * groups; otherwise adds a blocking progress spinner with an appropriate * message. */ @ThreadConfined(type = ThreadConfined.ThreadType.JFX) @NbBundle.Messages({ "ImageGalleryController.noGroupsDlg.msg1=No groups are fully analyzed; but listening to ingest is disabled. " + " No groups will be available until ingest is finished and listening is re-enabled.", "ImageGalleryController.noGroupsDlg.msg2=No groups are fully analyzed yet, but ingest is still ongoing. Please Wait.", "ImageGalleryController.noGroupsDlg.msg3=No groups are fully analyzed yet, but image / video data is still being populated. Please Wait.", "ImageGalleryController.noGroupsDlg.msg4=There are no images/videos available from the added datasources; but listening to ingest is disabled. " + " No groups will be available until ingest is finished and listening is re-enabled.", "ImageGalleryController.noGroupsDlg.msg5=There are no images/videos in the added datasources.", "ImageGalleryController.noGroupsDlg.msg6=There are no fully analyzed groups to display:" + " the current Group By setting resulted in no groups, " + "or no groups are fully analyzed but ingest is not running."}) private void checkForAnalyzedGroups() { synchronized (controllerLock) { GroupManager groupManager = controller.getGroupManager(); // if there are groups to display, then display them // @@@ Need to check timing on this and make sure we have only groups for the selected DS. Seems like rebuild can cause groups to be created for a DS that is not later selected... // RJCTODO: Get Brian's TODO resolved. if (isNotEmpty(groupManager.getAnalyzedGroups())) { clearNotification(); return; } // display a message based on if ingest is running and/or listening if (IngestManager.getInstance().isIngestRunning()) { if (controller.isListeningEnabled()) { replaceNotification(centralStack, new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg2(), new ProgressIndicator())); } else { replaceNotification(fullUIStack, new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg1())); } return; } // display a message about stuff still being in the queue if (controller.getDBTasksQueueSizeProperty().get() > 0) { replaceNotification(fullUIStack, new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg3(), new ProgressIndicator())); return; } // are there are files in the DB? try { if (controller.getDatabase().countAllFiles() <= 0) { // there are no files in db if (controller.isListeningEnabled()) { replaceNotification(fullUIStack, new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg5())); } else { replaceNotification(fullUIStack, new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg4())); } return; } } catch (TskCoreException tskCoreException) { logger.log(Level.SEVERE, "Error counting files in the database.", tskCoreException); } if (false == groupManager.isRegrouping()) { replaceNotification(centralStack, new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg6())); } } } @ThreadConfined(type = ThreadConfined.ThreadType.JFX) private void replaceNotification(StackPane stackPane, Node newNode) { clearNotification(); infoOverlay = new StackPane(infoOverLayBackground, newNode); if (stackPane != null) { stackPane.getChildren().add(infoOverlay); } } /** * Removes the spinner(s). */ @ThreadConfined(type = ThreadConfined.ThreadType.JFX) private void clearNotification() { fullUIStack.getChildren().remove(infoOverlay); centralStack.getChildren().remove(infoOverlay); } /** * A partially opaque region used to block out parts of the UI behind a * pseudo dialog. */ static final private class TranslucentRegion extends Region { TranslucentRegion() { setBackground(new Background(new BackgroundFill(Color.GREY, CornerRadii.EMPTY, Insets.EMPTY))); setOpacity(.4); } } }