/* * Autopsy Forensic Browser * * Copyright 2015 Basis Technology Corp. * Contact: carrier <at> sleuthkit <dot> org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.sleuthkit.autopsy.experimental.autoingest; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import javax.swing.SwingUtilities; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; import org.openide.util.actions.CallableSystemAction; import org.sleuthkit.autopsy.casemodule.AddImageAction; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.CaseActionException; import org.sleuthkit.autopsy.casemodule.CaseNewAction; import org.sleuthkit.autopsy.experimental.configuration.AutoIngestUserPreferences; import org.sleuthkit.autopsy.experimental.coordinationservice.CoordinationService; import org.sleuthkit.autopsy.experimental.coordinationservice.CoordinationService.CoordinationServiceException; /** * Handles opening, locking, and unlocking cases in review mode. Instances of * this class are tightly coupled to the Autopsy "current case" concept and the * Autopsy UI, and cases must be opened by code executing in the event * dispatch thread (EDT). Because of the tight coupling to the UI, exception * messages are deliberately user-friendly. */ final class ReviewModeCaseManager implements PropertyChangeListener { /* * Provides uniform exceptions with user-friendly error messages. */ final class ReviewModeCaseManagerException extends Exception { private static final long serialVersionUID = 1L; private ReviewModeCaseManagerException(String message) { super(message); } private ReviewModeCaseManagerException(String message, Throwable cause) { super(message, cause); } } private static final Logger logger = Logger.getLogger(ReviewModeCaseManager.class.getName()); private static ReviewModeCaseManager instance; private CoordinationService.Lock currentCaseLock; /** * Gets the review mode case manager. * * @return The review mode case manager singleton. */ synchronized static ReviewModeCaseManager getInstance() { if (instance == null) { /* * Two stage construction is used here to avoid allowing "this" * reference to escape from the constructor via registering as an * PropertyChangeListener. This is to ensure that a partially * constructed manager is not published to other threads. */ instance = new ReviewModeCaseManager(); Case.addPropertyChangeListener(instance); } return instance; } /** * Constructs a review mode case manager to handles opening, locking, and * unlocking cases in review mode. Instances of this class are tightly * coupled to the Autopsy "current case" concept and the Autopsy UI, * and cases must be opened by code executing in the event dispatch thread * (EDT). Because of the tight coupling to the UI, exception messages are * deliberately user-friendly. * */ private ReviewModeCaseManager() { /* * Disable the new case action because review mode is only for looking * at cases created by automated ingest. */ CallableSystemAction.get(CaseNewAction.class).setEnabled(false); /* * Permanently delete the "Open Recent Cases" item in the "File" menu. * This is quite drastic, as it also affects Autopsy standalone mode on * this machine, but review mode is only for looking at cases created by * automated ingest. */ FileObject root = FileUtil.getConfigRoot(); FileObject openRecentCasesMenu = root.getFileObject("Menu/Case/OpenRecentCase"); if (openRecentCasesMenu != null) { try { openRecentCasesMenu.delete(); } catch (IOException ex) { ReviewModeCaseManager.logger.log(Level.WARNING, "Unable to remove Open Recent Cases file menu item", ex); } } } /* * Gets a list of the cases in the top level case folder used by automated * ingest. */ List<AutoIngestCase> getCases() { List<AutoIngestCase> cases = new ArrayList<>(); List<Path> caseFolders = PathUtils.findCaseFolders(Paths.get(AutoIngestUserPreferences.getAutoModeResultsFolder())); for (Path caseFolderPath : caseFolders) { cases.add(new AutoIngestCase(caseFolderPath)); } return cases; } /** * Attempts to open a case as the current case. Assumes it is called by code * executing in the event dispatch thread (EDT). * * @param caseMetadataFilePath Path to the case metadata file. * * @throws ReviewModeCaseManagerException */ /* * TODO (RC): With a little work, the lock acquisition/release could be done * by a thread in a single thread executor, removing the "do it in the EDT" * requirement */ synchronized void openCaseInEDT(Path caseMetadataFilePath) throws ReviewModeCaseManagerException { Path caseFolderPath = caseMetadataFilePath.getParent(); try { /* * Acquire a lock on the case folder. If the lock cannot be * acquired, the case cannot be opened. */ currentCaseLock = CoordinationService.getInstance(CoordinationServiceNamespace.getRoot()).tryGetSharedLock(CoordinationService.CategoryNode.CASES, caseFolderPath.toString()); if (null == currentCaseLock) { throw new ReviewModeCaseManagerException("Could not get shared access to multi-user case folder"); } /* * Open the case. */ Case.open(caseMetadataFilePath.toString()); /** * Disable the add data source action in review mode. This has to be * done here because Case.open() calls Case.doCaseChange() and the * latter method enables the action. Since Case.doCaseChange() * enables the menus on EDT by calling SwingUtilities.invokeLater(), * we have to do the same thing here to maintain the order of * execution. */ SwingUtilities.invokeLater(() -> { CallableSystemAction.get(AddImageAction.class).setEnabled(false); }); } catch (CoordinationServiceException | ReviewModeCaseManagerException | CaseActionException ex) { /* * Release the coordination service lock on the case folder. */ try { if (currentCaseLock != null) { currentCaseLock.release(); currentCaseLock = null; } } catch (CoordinationService.CoordinationServiceException exx) { logger.log(Level.SEVERE, String.format("Error deleting legacy LOCKED state file for case at %s", caseFolderPath), exx); } if (ex instanceof CoordinationServiceException) { throw new ReviewModeCaseManagerException("Could not get access to the case folder from the coordination service, contact administrator", ex); } else if (ex instanceof IOException) { throw new ReviewModeCaseManagerException("Could not write to the case folder, contact adminstrator", ex); } else if (ex instanceof CaseActionException) { /* * CaseActionExceptions have user friendly error messages. */ throw new ReviewModeCaseManagerException(String.format("Could not open the case (%s), contract administrator", ex.getMessage()), ex); } else if (ex instanceof ReviewModeCaseManagerException) { throw (ReviewModeCaseManagerException) ex; } } } /** * @inheritDoc */ @Override public void propertyChange(PropertyChangeEvent evt) { if (evt.getPropertyName().equals(Case.Events.CURRENT_CASE.toString()) && null != evt.getOldValue() && null == evt.getNewValue()) { /* * When a case is closed, release the coordination service lock on * the case folder. This must be done in the EDT because it was * acquired in the EDT via openCase(). */ if (null != currentCaseLock) { try { SwingUtilities.invokeAndWait(() -> { try { currentCaseLock.release(); currentCaseLock = null; } catch (CoordinationService.CoordinationServiceException ex) { logger.log(Level.SEVERE, String.format("Failed to release the coordination service lock with path %s", currentCaseLock.getNodePath()), ex); currentCaseLock = null; } }); } catch (InterruptedException | InvocationTargetException ex) { logger.log(Level.SEVERE, String.format("Failed to release the coordination service lock with path %s", currentCaseLock.getNodePath()), ex); currentCaseLock = null; } } } } }