diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties index be0ddd285b..ca31fb4c75 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties @@ -96,10 +96,10 @@ Case.databaseConnectionInfo.error.msg=Error accessing database server connection Case.open.msgDlg.updated.msg=Updated case database schema.\nA backup copy of the database with the following path has been made\:\n {0} Case.open.msgDlg.updated.title=Case Database Schema Update Case.open.exception.multiUserCaseNotEnabled=Cannot open a multi-user case if multi-user cases are not enabled. See Tools, Options, Multi-user. -Case.checkImgExist.confDlg.doesntExist.msg={0} has detected that one of the images associated with \n\ +Case.checkImgExist.confDlg.doesntExist.msg=One of the images associated with \n\ this case are missing. Would you like to search for them now?\n\ Previously, the image was located at\:\n\ -{1}\n\ +{0}\n\ Please note that you will still be able to browse directories and generate reports\n\ if you choose No, but you will not be able to view file content or run the ingest process. Case.checkImgExist.confDlg.doesntExist.title=Missing Image @@ -136,12 +136,6 @@ IntervalErrorReport.NewIssues=new issue(s) IntervalErrorReport.TotalIssues=total issue(s) IntervalErrorReport.ErrorText=Database Connection Error CasePropertiesAction.window.title=Case Properties -CasePropertiesForm.updateCaseName.msgDlg.empty.msg=The caseName cannot be empty. -CasePropertiesForm.updateCaseName.msgDlg.empty.title=Error -CasePropertiesForm.updateCaseName.msgDlg.invalidSymbols.msg=The Case Name cannot contain any of this following symbol\: \\ / \: * ? " < > | -CasePropertiesForm.updateCaseName.msgDlg.invalidSymbols.title=Error -CasePropertiesForm.updateCaseName.confMsg.msg=Are you sure you want to update the case name from "{0}" to "{1}"? -CasePropertiesForm.updateCaseName.confMsg.title=Change Case Name CueBannerPanel.title.text=Open Recent Case GeneralFilter.rawImageDesc.text=Raw Images (*.img, *.dd, *.001, *.aa, *.raw, *.bin) GeneralFilter.encaseImageDesc.text=Encase Images (*.e01) diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle_ja.properties b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle_ja.properties index 989fd8a29f..60787ada8c 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle_ja.properties +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle_ja.properties @@ -73,10 +73,6 @@ Case.getCurCase.exception.noneOpen=\u4f5c\u696d\u4e2d\u306e\u30b1\u30fc\u30b9\u3 Case.open.msgDlg.updated.msg=\u30b1\u30fc\u30b9\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u306e\u30b9\u30ad\u30fc\u30de\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f\u3002\n\u6b21\u306e\u30d1\u30b9\u3092\u6301\u3064\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u306e\u30d0\u30c3\u30af\u30a2\u30c3\u30d7\u30b3\u30d4\u30fc\u304c\u4f5c\u6210\u3055\u308c\u307e\u3057\u305f\uff1a\n\ {0} Case.open.msgDlg.updated.title=\u30b1\u30fc\u30b9\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u306e\u30b9\u30ad\u30fc\u30de\u3092\u66f4\u65b0 -Case.checkImgExist.confDlg.doesntExist.msg={0} \u304c\u3053\u306e\u30b1\u30fc\u30b9\u306b\u95a2\u9023\u3059\u308b\u30a4\u30e1\u30fc\u30b8\u306e\u3046\u3061\uff11\u3064\u304c\u6b20\u843d\u3057\u3066\u3044\u308b\u306e\u3092\u691c\u51fa\u3057\u307e\u3057\u305f\u3002\u305d\u308c\u3092\u4eca\u304b\u3089\u691c\u7d22\u3057\u307e\u3059\u304b\uff1f\n\n\ -\u4ee5\u524d\u3001\u30a4\u30e1\u30fc\u30b8\u306f\u6b21\u306b\u3042\u308a\u307e\u3057\u305f\uff1a\n\ -{1}\n\ -\u3044\u3044\u3048\u3092\u9078\u629e\u3057\u3066\u3082\u3001\u4eca\u5f8c\u3082\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u3092\u95b2\u89a7\u3057\u3001\u30ec\u30dd\u30fc\u30c8\u751f\u6210\u304c\u3067\u304d\u307e\u3059\u304c\u3001\n\u30d5\u30a1\u30a4\u30eb\u30b3\u30f3\u30c6\u30f3\u30c4\u306e\u8868\u793a\u307e\u305f\u306f\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u30d7\u30ed\u30bb\u30b9\u306e\u5b9f\u884c\u304c\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002 Case.checkImgExist.confDlg.doesntExist.title=\u6b20\u843d\u3057\u3066\u3044\u308b\u30a4\u30e1\u30fc\u30b8 Case.addImg.exception.msg=\u30b1\u30fc\u30b9\u306b\u30a4\u30e1\u30fc\u30b8\u3092\u8ffd\u52a0\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f Case.updateCaseName.exception.msg=\u30b1\u30fc\u30b9\u540d\u3092\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002 @@ -99,10 +95,6 @@ CaseDeleteAction.msgDlg.caseDelete.msg=\u30b1\u30fc\u30b9\u304c\u524a\u9664\u305 CaseOpenAction.autFilter.title={0} \u30b1\u30fc\u30b9\u30d5\u30a1\u30a4\u30eb ( {1}) CaseOpenAction.msgDlg.cantOpenCase.title=\u30b1\u30fc\u30b9\u3092\u958b\u304f\u969b\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f CasePropertiesAction.window.title=\u30b1\u30fc\u30b9\u30d7\u30ed\u30d1\u30c6\u30a3 -CasePropertiesForm.updateCaseName.msgDlg.empty.msg=\u30b1\u30fc\u30b9\u540d\u306f\u7a7a\u767d\u3067\u306f\u3044\u3051\u307e\u305b\u3093\u3002 -CasePropertiesForm.updateCaseName.msgDlg.empty.title=\u30a8\u30e9\u30fc -CasePropertiesForm.updateCaseName.msgDlg.invalidSymbols.msg=\u30b1\u30fc\u30b9\u540d\u306b\u306f\u6b21\u306e\u8a18\u53f7\u3092\u542b\u3081\u307e\u305b\u3093\uff1a\\ / \: * ? " < > | -CasePropertiesForm.updateCaseName.msgDlg.invalidSymbols.title=\u30a8\u30e9\u30fc CueBannerPanel.title.text=\u6700\u8fd1\u958b\u3044\u305f\u30b1\u30fc\u30b9\u3092\u958b\u304f GeneralFilter.rawImageDesc.text=\u30ed\u30fc\u30a4\u30e1\u30fc\u30b8(*.img, *.dd, *.001, *.aa, *.raw, *.bin) GeneralFilter.encaseImageDesc.text=\u30a8\u30f3\u30b1\u30fc\u30b9\u30a4\u30e1\u30fc\u30b8(*.e01) diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java index 08880d9b9b..5bd1618859 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java @@ -25,12 +25,12 @@ import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.File; import java.io.IOException; -import java.lang.reflect.InvocationTargetException; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; import java.sql.Connection; import java.sql.DriverManager; +import java.sql.SQLException; import java.sql.Statement; import java.text.SimpleDateFormat; import java.util.Collection; @@ -48,11 +48,13 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import org.openide.util.Lookup; @@ -98,6 +100,7 @@ import org.sleuthkit.autopsy.framework.ProgressIndicator; import org.sleuthkit.autopsy.ingest.IngestJob; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.keywordsearchservice.KeywordSearchService; +import org.sleuthkit.autopsy.keywordsearchservice.KeywordSearchServiceException; import org.sleuthkit.autopsy.timeline.OpenTimelineAction; import org.sleuthkit.datamodel.BlackboardArtifactTag; import org.sleuthkit.datamodel.CaseDbConnectionInfo; @@ -124,45 +127,18 @@ public class Case { private static final String TEMP_FOLDER = "Temp"; //NON-NLS private static final int MIN_SECS_BETWEEN_TSK_ERROR_REPORTS = 60; private static final String MODULE_FOLDER = "ModuleOutput"; //NON-NLS + private static final long EXECUTOR_AWAIT_TIMEOUT_SECS = 5; + private static final String CASE_ACTION_THREAD_NAME = "%s-case-action"; + private static final String CASE_RESOURCES_THREAD_NAME = "%s-manage-case-resources"; private static final Logger logger = Logger.getLogger(Case.class.getName()); private static final AutopsyEventPublisher eventPublisher = new AutopsyEventPublisher(); - - /* - * The following fields are the mutable state associated with the current - * case concept. The currentCase field is guarded for writes by the - * currentCaseWriteLock. The field is also volatile to allow non-locking - * reads via the isCaseOpen and getCurrentCase methods. This is unfortunate, - * but Case clients that do not respond correctly to CURRENT_CASE closed - * events may call these methods and that would be a source of potential - * deadlock if the currentCaseWriteLock was used to guard read access. - * - * TODO (JIRA-2228): Throw CaseClosedException from Case instance methods. - */ - private static final Object currentCaseWriteLock = new Object(); - @GuardedBy("currentCaseWriteLock") + private static final Object caseActionSerializationLock = new Object(); + private static volatile Frame mainFrame; + private static volatile String appName; private static volatile Case currentCase; - - /* - * The application name, used to make the title of the main application - * window [application name] when there is no open case and [curent case - * display name] - [application name] when there is an open case. - * Initialized by getting the main window title before a case has been - * opened. A reference to the main window frame is obtained at the same time - * as a convenmient side effect for parenting dialogs. - * - * TODO (JIRA-2231): Make the application name a RuntimeProperties item. - */ - @GuardedBy("currentCaseWriteLock") - private static Frame mainFrame; - @GuardedBy("currentCaseWriteLock") - private static String appName; - - /* - * Case instance data. - */ - private CaseMetadata caseMetadata; + private final CaseMetadata metadata; + private volatile ExecutorService caseLockingExecutor; private CoordinationService.Lock caseDirLock; - private ExecutorService caseLockingExecutor; private SleuthkitCase caseDb; private SleuthkitErrorReporter sleuthkitErrorReporter; private CollaborationMonitor collaborationMonitor; @@ -319,9 +295,6 @@ public class Case { * interaction with a closed case, the case in the old value should be * used, and it should be done synchronously in the CURRENT_CASE event * handler. - * - * TODO (JIRA-2228): Throw CaseClosedException from Case instance - * methods. */ CURRENT_CASE, /** @@ -458,41 +431,22 @@ public class Case { * the empty string. * @param caseType The type of case (single-user or multi-user). * - * @throws CaseActionException if there is a problem creating the case. The - * exception will have a user-friendly message - * and may be a wrapper for a lower-level - * exception. + * @throws CaseActionException If there is a problem creating the + * case. + * @throws CaseActionCancelledException If creating the case is cancelled. */ @Messages({ - "# {0} - exception message", "Case.exceptionMessage.wrapperMessage={0}" + "Case.exceptionMessage.emptyCaseName=Must specify a case name.", + "Case.exceptionMessage.emptyCaseDir=Must specify a case directory path." }) - public static void createAsCurrentCase(String caseDir, String caseDisplayName, String caseNumber, String examiner, CaseType caseType) throws CaseActionException { - synchronized (currentCaseWriteLock) { - if (RuntimeProperties.runningWithGUI()) { - getMainWindowAndAppName(); - } - - if (null != currentCase) { - String previousCaseDisplayName = currentCase.getDisplayName(); - String previousCaseName = currentCase.getName(); - String previousCaseDir = currentCase.getCaseDirectory(); - try { - closeCurrentCase(); - } catch (CaseActionException ex) { - logger.log(Level.SEVERE, String.format("Error closing the previous current case %s (%s) in %s", previousCaseDisplayName, previousCaseName, previousCaseDir), ex); //NON-NLS - } - } - - logger.log(Level.INFO, "Creating current case {0} in {1}", new Object[]{caseDisplayName, caseDir}); //NON-NLS - Case newCurrentCase = new Case(); - newCurrentCase.create(caseType, caseDir, caseDisplayName, caseNumber, examiner); - currentCase = newCurrentCase; - logger.log(Level.INFO, "Created currrent case {0} ({1}) in {2}", new Object[]{newCurrentCase.getDisplayName(), newCurrentCase.getName(), newCurrentCase.getCaseDirectory()}); //NON-NLS - if (RuntimeProperties.runningWithGUI()) { - updateGUIForCaseOpened(newCurrentCase); - } - eventPublisher.publishLocally(new AutopsyEvent(Events.CURRENT_CASE.toString(), null, newCurrentCase)); + public static void createAsCurrentCase(String caseDir, String caseDisplayName, String caseNumber, String examiner, CaseType caseType) throws CaseActionException, CaseActionCancelledException { + if (caseDisplayName.isEmpty()) { + throw new CaseActionException(Bundle.Case_exceptionMessage_emptyCaseName()); } + if (caseDir.isEmpty()) { + throw new CaseActionException(Bundle.Case_exceptionMessage_emptyCaseDir()); + } + openAsCurrentCase(new Case(caseType, caseDir, caseDisplayName, caseNumber, examiner), true); } /** @@ -503,40 +457,22 @@ public class Case { * * @param caseMetadataFilePath The path of the case metadata (.aut) file. * - * @throws CaseActionException if there is a problem opening the case. The + * @throws CaseActionException If there is a problem opening the case. The * exception will have a user-friendly message * and may be a wrapper for a lower-level * exception. */ @Messages({ - "# {0} - exception message", "Case.openException.couldNotOpenCase=Could not open case: {0}", - "Case.progressIndicatorTitle.openingCase=Opening Case", - "Case.exceptionMessage.failedToReadMetadata=Failed to read metadata." + "Case.exceptionMessage.failedToReadMetadata=Failed to read case metadata." }) public static void openAsCurrentCase(String caseMetadataFilePath) throws CaseActionException { - synchronized (currentCaseWriteLock) { - if (RuntimeProperties.runningWithGUI()) { - getMainWindowAndAppName(); - } - - if (null != currentCase) { - try { - closeCurrentCase(); - } catch (CaseActionException ex) { - logger.log(Level.SEVERE, "Error closing the previous current case", ex); - } - } - - Case newCurrentCase = new Case(); - logger.log(Level.INFO, "Opening case with metadata file path {0} as current case", caseMetadataFilePath); //NON-NLS - newCurrentCase.open(Paths.get(caseMetadataFilePath)); - currentCase = newCurrentCase; - logger.log(Level.INFO, "Opened case with metadata file path {0} as current case", caseMetadataFilePath); //NON-NLS - if (RuntimeProperties.runningWithGUI()) { - updateGUIForCaseOpened(newCurrentCase); - } - eventPublisher.publishLocally(new AutopsyEvent(Events.CURRENT_CASE.toString(), null, currentCase)); + CaseMetadata metadata; + try { + metadata = new CaseMetadata(Paths.get(caseMetadataFilePath)); + } catch (CaseMetadataException ex) { + throw new CaseActionException(Bundle.Case_exceptionMessage_failedToReadMetadata(), ex); } + openAsCurrentCase(new Case(metadata), false); } /** @@ -556,48 +492,66 @@ public class Case { * @throws IllegalStateException if there is no current case. */ public static Case getCurrentCase() { - Case caseToReturn = currentCase; - if (null != caseToReturn) { - return caseToReturn; + /* + * Throwing an unchecked exception is a bad idea here. + * + * TODO (JIRA-2229): Case.getCurrentCase() method throws unchecked + * IllegalStateException; change to throw checked exception or return + * null + */ + if (null != currentCase) { + return currentCase; } else { throw new IllegalStateException(NbBundle.getMessage(Case.class, "Case.getCurCase.exception.noneOpen")); } } /** - * Closes the current case if there is a current case. + * Closes the current case. * - * IMPORTANT: This method should not be called in the event dispatch thread - * (EDT). - * - * @throws CaseActionException + * @throws CaseActionException If there is a problem closing the case. The + * exception will have a user-friendly message + * and may be a wrapper for a lower-level + * exception. */ @Messages({ "# {0} - exception message", "Case.closeException.couldNotCloseCase=Error closing case: {0}", "Case.progressIndicatorTitle.closingCase=Closing Case" }) public static void closeCurrentCase() throws CaseActionException { - synchronized (currentCaseWriteLock) { + synchronized (caseActionSerializationLock) { if (null == currentCase) { return; } - String caseName = currentCase.getName(); - String caseDir = currentCase.getCaseDirectory(); + Case closedCase = currentCase; try { - Case closedCase = currentCase; eventPublisher.publishLocally(new AutopsyEvent(Events.CURRENT_CASE.toString(), closedCase, null)); - logger.log(Level.INFO, "Closing current case {0} in {1}", new Object[]{caseName, caseDir}); //NON-NLS + logger.log(Level.INFO, "Closing current case {0} ({1}) in {2}", new Object[]{closedCase.getDisplayName(), closedCase.getName(), closedCase.getCaseDirectory()}); //NON-NLS currentCase = null; closedCase.close(); + logger.log(Level.INFO, "Closed current case {0} ({1}) in {2}", new Object[]{closedCase.getDisplayName(), closedCase.getName(), closedCase.getCaseDirectory()}); //NON-NLS + } catch (CaseActionException ex) { + logger.log(Level.SEVERE, String.format("Error closing current case %s (%s) in %s", closedCase.getDisplayName(), closedCase.getName(), closedCase.getCaseDirectory()), ex); //NON-NLS + throw ex; } finally { - /* - * The case is no longer the current case, even if an exception - * was thrown. - */ - logger.log(Level.INFO, "Closed current case {0} in {1}", new Object[]{caseName, caseDir}); //NON-NLS if (RuntimeProperties.runningWithGUI()) { updateGUIForCaseClosed(); } + + /* + * This is an undocumented, legacy hack. Empirically, it seems + * to be necessary due to problems with finalizers in the + * SleuthKit Java bindings data model calling native methods + * that read garbage from freed memory, leading to access + * violations otherwise. Why the garbage collector is called + * twice is not known, but it appears to be intended to try to + * force the garbage collection to occur. + * + * TODO (JIRA-2611): Make JNI code more robust when handling + * file closure + */ + System.gc(); + System.gc(); } } } @@ -605,20 +559,17 @@ public class Case { /** * Deletes the current case. * - * IMPORTANT: This method should not be called in the event dispatch thread - * (EDT). - * - * @throws CaseActionException if there is a problem deleting the case. The + * @throws CaseActionException If there is a problem deleting the case. The * exception will have a user-friendly message * and may be a wrapper for a lower-level * exception. */ public static void deleteCurrentCase() throws CaseActionException { - synchronized (currentCaseWriteLock) { + synchronized (caseActionSerializationLock) { if (null == currentCase) { return; } - CaseMetadata metadata = currentCase.getCaseMetadata(); + CaseMetadata metadata = currentCase.getMetadata(); closeCurrentCase(); deleteCase(metadata); } @@ -638,38 +589,31 @@ public class Case { @Messages({ "# {0} - exception message", "Case.deleteException.couldNotDeleteCase=Could not delete case: {0}", "Case.progressIndicatorTitle.deletingCase=Deleting Case", - "Case.exceptionMessage.cannotDeleteCurrentCase=Cannot delete current case, it must be closed first", + "Case.exceptionMessage.cannotDeleteCurrentCase=Cannot delete current case, it must be closed first.", "Case.progressMessage.checkingForOtherUser=Checking to see if another user has the case open...", - "Case.progressMessage.deletingTextIndex=Deleting text index...", - "Case.progressMessage.deletingCaseDatabase=Deleting case database...", - "Case.exceptionMessage.cancelled=Cancelled by user" - }) + "Case.exceptionMessage.cannotDeleteCaseOpenForOtherUser=Cannot delete the case, it is open for another user.",}) public static void deleteCase(CaseMetadata metadata) throws CaseActionException { - synchronized (currentCaseWriteLock) { + synchronized (caseActionSerializationLock) { if (null != currentCase && 0 == metadata.getCaseDirectory().compareTo(metadata.getCaseDirectory())) { throw new CaseActionException(Bundle.Case_deleteException_couldNotDeleteCase(Bundle.Case_exceptionMessage_cannotDeleteCurrentCase())); } } /* - * Set up either a GUI progress indicator or a logging progress - * indicator. + * Set up either a GUI progress indicator without a cancel button (can't + * cancel deleting a case) or a logging progress indicator. */ ProgressIndicator progressIndicator; if (RuntimeProperties.runningWithGUI()) { - progressIndicator = new ModalDialogProgressIndicator( - mainFrame, - Bundle.Case_progressIndicatorTitle_deletingCase()); + progressIndicator = new ModalDialogProgressIndicator(mainFrame, Bundle.Case_progressIndicatorTitle_deletingCase()); } else { progressIndicator = new LoggingProgressIndicator(); } progressIndicator.start(Bundle.Case_progressMessage_preparing()); - logger.log(Level.INFO, "Deleting case with metadata file path {0}", metadata.getFilePath()); //NON-NLS - ExecutorService executor = Executors.newSingleThreadExecutor(); - Future future = executor.submit(() -> { + try { if (CaseType.SINGLE_USER_CASE == metadata.getCaseType()) { - cleanupDeletedCase(metadata, progressIndicator); + deleteCase(metadata, progressIndicator); } else { /* * First, acquire an exclusive case directory lock. The case @@ -678,48 +622,79 @@ public class Case { progressIndicator.start(Bundle.Case_progressMessage_checkingForOtherUser()); try (CoordinationService.Lock dirLock = CoordinationService.getInstance().tryGetExclusiveLock(CategoryNode.CASES, metadata.getCaseDirectory())) { assert (null != dirLock); - - /* - * Delete the text index. - */ - progressIndicator.start(Bundle.Case_progressMessage_deletingTextIndex()); - for (KeywordSearchService searchService : Lookup.getDefault().lookupAll(KeywordSearchService.class)) { - searchService.deleteTextIndex(metadata); - } - - if (CaseType.MULTI_USER_CASE == metadata.getCaseType()) { - /* - * Delete the case database from the database server. - * The case database for a single-user case is in the - * case directory and will be deleted whe it is deleted. - */ - progressIndicator.start(Bundle.Case_progressMessage_deletingCaseDatabase()); - CaseDbConnectionInfo db = UserPreferences.getDatabaseConnectionInfo(); - Class.forName("org.postgresql.Driver"); //NON-NLS - try (Connection connection = DriverManager.getConnection("jdbc:postgresql://" + db.getHost() + ":" + db.getPort() + "/postgres", db.getUserName(), db.getPassword()); //NON-NLS - Statement statement = connection.createStatement();) { - String deleteCommand = "DROP DATABASE \"" + metadata.getCaseDatabaseName() + "\""; //NON-NLS - statement.execute(deleteCommand); - } - } - - cleanupDeletedCase(metadata, progressIndicator); + deleteCase(metadata, progressIndicator); + } catch (CoordinationServiceException ex) { } } - return null; - }); - - try { - future.get(); - logger.log(Level.INFO, "Deleted case with metadata file path {0}", metadata.getFilePath()); //NON-NLS - } catch (InterruptedException ex) { - throw new CaseActionException(Bundle.Case_exceptionMessage_wrapperMessage(ex.getMessage()), ex); - } catch (ExecutionException ex) { - throw new CaseActionException(Bundle.Case_exceptionMessage_wrapperMessage(ex.getCause().getMessage()), ex); - } catch (CancellationException ex) { - throw new CaseActionException(Bundle.Case_exceptionMessage_cancelled(), ex); } finally { - executor.shutdown(); + progressIndicator.finish(); + } + } + + /** + * Opens a new or existing case as the current case. + * + * @param newCurrentCase The case. + * @param isNewCase True for a new case, false otherwise. + * + * @throws CaseActionException If there is a problem creating the + * case. + * @throws CaseActionCancelledException If creating the case is cancelled. + */ + @Messages({ + "Case.exceptionMessage.cannotLocateMainWindow=Cannot locate main application window" + }) + private static void openAsCurrentCase(Case newCurrentCase, boolean isNewCase) throws CaseActionException, CaseActionCancelledException { + if (RuntimeProperties.runningWithGUI() && null == mainFrame) { + /* + * Get a reference to the main window of the desktop application to + * use to parent pop up dialogs and initialize the application name + * for use in changing the main window title. This is tricky and + * fragile. The application name aspect can be resolved thus: + * + * TODO (JIRA-2231): Make the application name a RuntimeProperties + * item set by Installers. + * + * And the getting of the main frame should be resolved when the + * code is refactored to separate the presentation layer fomr the + * business layer. + * + * TODO (JIRA-multiple): Make it possible to run "headless." + */ + assert (!SwingUtilities.isEventDispatchThread()); + SwingUtilities.invokeLater(() -> { + mainFrame = WindowManager.getDefault().getMainWindow(); + appName = mainFrame.getTitle(); + }); + } + synchronized (caseActionSerializationLock) { + if (null != currentCase) { + try { + closeCurrentCase(); + } catch (CaseActionException ex) { + /* + * Notify the user and continue (the error has already been + * logged in closeCurrentCase. + */ + MessageNotifyUtil.Message.error(ex.getLocalizedMessage()); + } + } + try { + logger.log(Level.INFO, "Opening {0} ({1}) in {2} as the current case", new Object[]{newCurrentCase.getDisplayName(), newCurrentCase.getName(), newCurrentCase.getCaseDirectory()}); //NON-NLS + newCurrentCase.open(isNewCase); + currentCase = newCurrentCase; + logger.log(Level.INFO, "Opened {0} ({1}) in {2} as the current case", new Object[]{newCurrentCase.getDisplayName(), newCurrentCase.getName(), newCurrentCase.getCaseDirectory()}); //NON-NLS + if (RuntimeProperties.runningWithGUI()) { + updateGUIForCaseOpened(newCurrentCase); + } + eventPublisher.publishLocally(new AutopsyEvent(Events.CURRENT_CASE.toString(), null, currentCase)); + } catch (CaseActionCancelledException ex) { + logger.log(Level.INFO, String.format("Cancelled opening %s (%s) in %s as the current case", newCurrentCase.getDisplayName(), newCurrentCase.getName(), newCurrentCase.getCaseDirectory())); //NON-NLS + throw ex; + } catch (CaseActionException ex) { + logger.log(Level.SEVERE, String.format("Error opening %s (%s) in %s as the current case", newCurrentCase.getDisplayName(), newCurrentCase.getName(), newCurrentCase.getCaseDirectory()), ex); //NON-NLS + throw ex; + } } } @@ -857,56 +832,69 @@ public class Case { } /** - * Gets a reference to the main window of the desktop application to use to - * parent pop ups and initializes the application name for use in changing - * the main window title. MUST be called BEFORE any case is opened or - * created. * - * @throws CaseActionException - */ - @Messages({ - "Case.exceptionMessage.cannotLocateMainWindow=Cannot locate main application window" - }) - private static void getMainWindowAndAppName() throws CaseActionException { - if (RuntimeProperties.runningWithGUI() && null == mainFrame) { - try { - SwingUtilities.invokeAndWait(() -> { - mainFrame = WindowManager.getDefault().getMainWindow(); - /* - * This is tricky and fragile. What looks like lazy - * initialization of the appName field is actually getting - * the application name from the main window title BEFORE a - * case has been opened and a case name has been included in - * the title. It is also very specific to the desktop GUI. - * - * TODO (JIRA-2231): Make the application name a - * RuntimeProperties item set by Installers. - */ - appName = mainFrame.getTitle(); - }); - } catch (InterruptedException | InvocationTargetException ex) { - throw new CaseActionException(Bundle.Case_exceptionMessage_wrapperMessage(Bundle.Case_exceptionMessage_cannotLocateMainWindow()), ex); - } - } - } - - /** * Deletes the case directory of a deleted case and removes the case form * the Recent Cases menu. * * @param metadata The case metadata. * @param progressIndicator A progress indicator. + * + * @throws UserPreferencesException if there is a problem getting the case + * databse connection info for a multi-user + * case. + * @throws ClassNotFoundException if there is a problem loading the JDBC + * driver for PostgreSQL for a multi-user + * case. + * @throws SQLException If there is a problem */ @Messages({ - "Case.progressMessage.deletingCaseDirectory=Deleting case directory..." + "Case.progressMessage.deletingTextIndex=Deleting text index...", + "Case.progressMessage.deletingCaseDatabase=Deleting case database...", + "Case.progressMessage.deletingCaseDirectory=Deleting case directory...", + "Case.exceptionMessage.errorsDeletingCase=Errors occured while deleting the case. See the application log for details" }) - private static void cleanupDeletedCase(CaseMetadata metadata, ProgressIndicator progressIndicator) { + private static void deleteCase(CaseMetadata metadata, ProgressIndicator progressIndicator) throws CaseActionException { + boolean errorsOccurred = false; + if (CaseType.MULTI_USER_CASE == metadata.getCaseType()) { + /* + * Delete the case database from the database server. + */ + try { + progressIndicator.start(Bundle.Case_progressMessage_deletingCaseDatabase()); + CaseDbConnectionInfo db; + db = UserPreferences.getDatabaseConnectionInfo(); + Class.forName("org.postgresql.Driver"); //NON-NLS + try (Connection connection = DriverManager.getConnection("jdbc:postgresql://" + db.getHost() + ":" + db.getPort() + "/postgres", db.getUserName(), db.getPassword()); //NON-NLS + Statement statement = connection.createStatement();) { + String deleteCommand = "DROP DATABASE \"" + metadata.getCaseDatabaseName() + "\""; //NON-NLS + statement.execute(deleteCommand); + } + } catch (UserPreferencesException | ClassNotFoundException | SQLException ex) { + logger.log(Level.SEVERE, String.format("Failed to delete case database %s for %s (%s) in %s", metadata.getCaseDatabaseName(), metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); + errorsOccurred = true; + } + } + + /* + * Delete the text index. + */ + progressIndicator.start(Bundle.Case_progressMessage_deletingTextIndex()); + for (KeywordSearchService searchService : Lookup.getDefault().lookupAll(KeywordSearchService.class)) { + try { + searchService.deleteTextIndex(metadata); + } catch (KeywordSearchServiceException ex) { + logger.log(Level.SEVERE, String.format("Failed to delete text index for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); + errorsOccurred = true; + } + } + /* * Delete the case directory. */ progressIndicator.start(Bundle.Case_progressMessage_deletingCaseDirectory()); if (!FileUtil.deleteDir(new File(metadata.getCaseDirectory()))) { - logger.log(Level.SEVERE, "Failed to fully delete case directory {0}", metadata.getCaseDirectory()); + logger.log(Level.SEVERE, String.format("Failed to delete case directory for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory())); + errorsOccurred = true; } /* @@ -917,6 +905,10 @@ public class Case { RecentCases.getInstance().removeRecentCase(metadata.getCaseDisplayName(), metadata.getFilePath().toString()); }); } + + if (errorsOccurred) { + throw new CaseActionException(Bundle.Case_exceptionMessage_errorsDeletingCase()); + } } /** @@ -938,8 +930,9 @@ public class Case { throw new CaseActionException(Bundle.Case_creationException_couldNotAcquireResourcesLock()); } return lock; - - } catch (InterruptedException | CoordinationServiceException ex) { + } catch (InterruptedException ex) { + throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser()); + } catch (CoordinationServiceException ex) { throw new CaseActionException(Bundle.Case_creationException_couldNotAcquireResourcesLock(), ex); } } @@ -948,149 +941,139 @@ public class Case { * Update the GUI to to reflect the current case. */ private static void updateGUIForCaseOpened(Case newCurrentCase) { - SwingUtilities.invokeLater(() -> { - /* - * If the case database was upgraded for a new schema and a backup - * database was created, notify the user. - */ - SleuthkitCase caseDb = newCurrentCase.getSleuthkitCase(); - String backupDbPath = caseDb.getBackupDatabasePath(); - if (null != backupDbPath) { - JOptionPane.showMessageDialog( - WindowManager.getDefault().getMainWindow(), - NbBundle.getMessage(Case.class, "Case.open.msgDlg.updated.msg", backupDbPath), - NbBundle.getMessage(Case.class, "Case.open.msgDlg.updated.title"), - JOptionPane.INFORMATION_MESSAGE); - } + if (RuntimeProperties.runningWithGUI()) { + SwingUtilities.invokeLater(() -> { + /* + * If the case database was upgraded for a new schema and a + * backup database was created, notify the user. + */ + SleuthkitCase caseDb = newCurrentCase.getSleuthkitCase(); + String backupDbPath = caseDb.getBackupDatabasePath(); + if (null != backupDbPath) { + JOptionPane.showMessageDialog( + mainFrame, + NbBundle.getMessage(Case.class, "Case.open.msgDlg.updated.msg", backupDbPath), + NbBundle.getMessage(Case.class, "Case.open.msgDlg.updated.title"), + JOptionPane.INFORMATION_MESSAGE); + } - /* - * Look for the files for the data sources listed in the case - * database and give the user the opportunity to locate any that are - * missing. - */ - Map imgPaths = getImagePaths(caseDb); - for (Map.Entry entry : imgPaths.entrySet()) { - long obj_id = entry.getKey(); - String path = entry.getValue(); - boolean fileExists = (new File(path).isFile() || DriveUtils.driveExists(path)); - - if (!fileExists) { - int response = JOptionPane.showConfirmDialog( - WindowManager.getDefault().getMainWindow(), - NbBundle.getMessage(Case.class, "Case.checkImgExist.confDlg.doesntExist.msg", appName, path), - NbBundle.getMessage(Case.class, "Case.checkImgExist.confDlg.doesntExist.title"), - JOptionPane.YES_NO_OPTION); - if (response == JOptionPane.YES_OPTION) { - MissingImageDialog.makeDialog(obj_id, caseDb); - } else { - logger.log(Level.SEVERE, "User proceeding with missing image files"); //NON-NLS + /* + * Look for the files for the data sources listed in the case + * database and give the user the opportunity to locate any that + * are missing. + */ + Map imgPaths = getImagePaths(caseDb); + for (Map.Entry entry : imgPaths.entrySet()) { + long obj_id = entry.getKey(); + String path = entry.getValue(); + boolean fileExists = (new File(path).isFile() || DriveUtils.driveExists(path)); + if (!fileExists) { + int response = JOptionPane.showConfirmDialog( + mainFrame, + NbBundle.getMessage(Case.class, "Case.checkImgExist.confDlg.doesntExist.msg", path), + NbBundle.getMessage(Case.class, "Case.checkImgExist.confDlg.doesntExist.title"), + JOptionPane.YES_NO_OPTION); + if (response == JOptionPane.YES_OPTION) { + MissingImageDialog.makeDialog(obj_id, caseDb); + } else { + logger.log(Level.SEVERE, "User proceeding with missing image files"); //NON-NLS + } } } - } - /* - * Enable the case-specific actions. - */ - CallableSystemAction.get(AddImageAction.class - ).setEnabled(true); - CallableSystemAction - .get(CaseCloseAction.class - ).setEnabled(true); - CallableSystemAction - .get(CasePropertiesAction.class - ).setEnabled(true); - CallableSystemAction - .get(CaseDeleteAction.class - ).setEnabled(true); - CallableSystemAction - .get(OpenTimelineAction.class - ).setEnabled(true); - CallableSystemAction - .get(OpenOutputFolderAction.class - ).setEnabled(false); + /* + * Enable the case-specific actions. + */ + CallableSystemAction.get(AddImageAction.class + ).setEnabled(true); + CallableSystemAction + .get(CaseCloseAction.class + ).setEnabled(true); + CallableSystemAction + .get(CasePropertiesAction.class + ).setEnabled(true); + CallableSystemAction + .get(CaseDeleteAction.class + ).setEnabled(true); + CallableSystemAction + .get(OpenTimelineAction.class + ).setEnabled(true); + CallableSystemAction + .get(OpenOutputFolderAction.class + ).setEnabled(false); - /* - * Add the case to the recent cases tracker that supplies a list of - * recent cases to the recent cases menu item and the open/create - * case dialog. - */ - RecentCases.getInstance().addRecentCase(newCurrentCase.getDisplayName(), newCurrentCase.getCaseMetadata().getFilePath().toString()); + /* + * Add the case to the recent cases tracker that supplies a list + * of recent cases to the recent cases menu item and the + * open/create case dialog. + */ + RecentCases.getInstance().addRecentCase(newCurrentCase.getDisplayName(), newCurrentCase.getMetadata().getFilePath().toString()); - /* - * Open the top components (windows within the main application - * window). - */ - if (newCurrentCase.hasData()) { - CoreComponentControl.openCoreWindows(); - } + /* + * Open the top components (windows within the main application + * window). + */ + if (newCurrentCase.hasData()) { + CoreComponentControl.openCoreWindows(); + } - /* - * Reset the main window title to be [curent case display name] - - * [application name], instead of just the application name. - */ - addCaseNameToMainWindowTitle(newCurrentCase.getDisplayName()); - }); + /* + * Reset the main window title to: + * + * [curent case display name] - [application name]. + */ + mainFrame.setTitle(newCurrentCase.getDisplayName() + " - " + appName); + }); + } } /* * Update the GUI to to reflect the lack of a current case. */ private static void updateGUIForCaseClosed() { - SwingUtilities.invokeLater(() -> { + if (RuntimeProperties.runningWithGUI()) { + SwingUtilities.invokeLater(() -> { + /* + * Close the top components (windows within the main application + * window). + */ + CoreComponentControl.closeCoreWindows(); - /* - * Close the top components (windows within the main application - * window). - */ - CoreComponentControl.closeCoreWindows(); + /* + * Disable the case-specific menu items. + */ + CallableSystemAction + .get(AddImageAction.class + ).setEnabled(false); + CallableSystemAction + .get(CaseCloseAction.class + ).setEnabled(false); + CallableSystemAction + .get(CasePropertiesAction.class + ).setEnabled(false); + CallableSystemAction + .get(CaseDeleteAction.class + ).setEnabled(false); + CallableSystemAction + .get(OpenTimelineAction.class + ).setEnabled(false); + CallableSystemAction + .get(OpenOutputFolderAction.class + ).setEnabled(false); - /* - * Disable the case-specific menu items. - */ - CallableSystemAction - .get(AddImageAction.class - ).setEnabled(false); - CallableSystemAction - .get(CaseCloseAction.class - ).setEnabled(false); - CallableSystemAction - .get(CasePropertiesAction.class - ).setEnabled(false); - CallableSystemAction - .get(CaseDeleteAction.class - ).setEnabled(false); - CallableSystemAction - .get(OpenTimelineAction.class - ).setEnabled(false); - CallableSystemAction - .get(OpenOutputFolderAction.class - ).setEnabled(false); + /* + * Clear the notifications in the notfier component in the lower + * right hand corner of the main application window. + */ + MessageNotifyUtil.Notify.clear(); - /* - * Clear the notifications in the notfier component in the lower - * right hand corner of the main application window. - */ - MessageNotifyUtil.Notify.clear(); - - /* - * Reset the main window title to be just the application name, - * instead of [curent case display name] - [application name]. - */ - Frame mainWindow = WindowManager.getDefault().getMainWindow(); - mainWindow.setTitle(appName); - }); - } - - /** - * Changes the title of the main window to include the case name. - * - * @param caseName The name of the case. - */ - private static void addCaseNameToMainWindowTitle(String caseName) { - if (!caseName.isEmpty()) { - Frame frame = WindowManager.getDefault().getMainWindow(); - frame.setTitle(caseName + " - " + appName); + /* + * Reset the main window title to be just the application name, + * instead of [curent case display name] - [application name]. + */ + mainFrame.setTitle(appName); + }); } } @@ -1137,7 +1120,7 @@ public class Case { * @return The case type. */ public CaseType getCaseType() { - return getCaseMetadata().getCaseType(); + return metadata.getCaseType(); } /** @@ -1146,7 +1129,7 @@ public class Case { * @return case The case create date. */ public String getCreatedDate() { - return getCaseMetadata().getCreatedDate(); + return metadata.getCreatedDate(); } /** @@ -1155,7 +1138,7 @@ public class Case { * @return The case name. */ public String getName() { - return getCaseMetadata().getCaseName(); + return metadata.getCaseName(); } /** @@ -1164,7 +1147,7 @@ public class Case { * @return The case display name. */ public String getDisplayName() { - return getCaseMetadata().getCaseDisplayName(); + return metadata.getCaseDisplayName(); } /** @@ -1173,7 +1156,7 @@ public class Case { * @return The case number */ public String getNumber() { - return caseMetadata.getCaseNumber(); + return metadata.getCaseNumber(); } /** @@ -1182,7 +1165,7 @@ public class Case { * @return The examiner name. */ public String getExaminer() { - return caseMetadata.getExaminer(); + return metadata.getExaminer(); } /** @@ -1191,7 +1174,7 @@ public class Case { * @return The top-level case directory path. */ public String getCaseDirectory() { - return caseMetadata.getCaseDirectory(); + return metadata.getCaseDirectory(); } /** @@ -1205,7 +1188,7 @@ public class Case { public String getOutputDirectory() { String caseDirectory = getCaseDirectory(); Path hostPath; - if (getCaseMetadata().getCaseType() == CaseType.MULTI_USER_CASE) { + if (CaseType.MULTI_USER_CASE == metadata.getCaseType()) { hostPath = Paths.get(caseDirectory, NetworkUtils.getLocalHostName()); } else { hostPath = Paths.get(caseDirectory); @@ -1329,23 +1312,13 @@ public class Case { } /** - * Sets the name of the keyword search index for the case. - * - * @param textIndexName The text index name. - * - * @throws CaseMetadataException - */ - public void setTextIndexName(String textIndexName) throws CaseMetadataException { - getCaseMetadata().setTextIndexName(textIndexName); - } - - /** - * Gets the name of the keyword search index for the case. + * Gets the name of the legacy keyword search index for the case. Not for + * general use. * * @return The index name. */ public String getTextIndexName() { - return getCaseMetadata().getTextIndexName(); + return getMetadata().getTextIndexName(); } /** @@ -1507,44 +1480,39 @@ public class Case { * * @return A CaseMetaData object. */ - CaseMetadata getCaseMetadata() { - return caseMetadata; + CaseMetadata getMetadata() { + return metadata; } /** * Updates the case display name name. - * - * @param oldCaseName The old case name. - * @param oldPath The old path name. - * @param newCaseName The new case name. - * @param newPath The new case path. */ - void updateCaseName(String oldCaseName, String oldPath, String newCaseName, String newPath) throws CaseActionException { + @Messages({ + "Case.exceptionMessage.metadataUpdateError=Failed to update case metadata, cannot change case display name." + }) + void updateDisplayName(String newDisplayName) throws CaseActionException { + String oldDisplayName = metadata.getCaseDisplayName(); try { - caseMetadata.setCaseDisplayName(newCaseName); - eventPublisher.publish(new AutopsyEvent(Events.NAME.toString(), oldCaseName, newCaseName)); + metadata.setCaseDisplayName(newDisplayName); + } catch (CaseMetadataException ex) { + throw new CaseActionException(Bundle.Case_exceptionMessage_metadataUpdateError()); + } + eventPublisher.publish(new AutopsyEvent(Events.NAME.toString(), oldDisplayName, newDisplayName)); + if (RuntimeProperties.runningWithGUI()) { SwingUtilities.invokeLater(() -> { + mainFrame.setTitle(newDisplayName + " - " + appName); try { - RecentCases.getInstance().updateRecentCase(oldCaseName, oldPath, newCaseName, newPath); - addCaseNameToMainWindowTitle(newCaseName); - + RecentCases.getInstance().updateRecentCase(oldDisplayName, metadata.getFilePath().toString(), newDisplayName, metadata.getFilePath().toString()); } catch (Exception ex) { - Logger.getLogger(Case.class - .getName()).log(Level.SEVERE, "Error updating case name in UI", ex); //NON-NLS + logger.log(Level.SEVERE, "Error updating case name in UI", ex); //NON-NLS } }); - } catch (CaseMetadataException ex) { - throw new CaseActionException(NbBundle.getMessage(this.getClass(), "Case.updateCaseName.exception.msg"), ex); } } /** - * Constructs an Autopsy case. - */ - private Case() { - } - - /** + * Constructs a Case object for a new Autopsy case. + * * @param caseType The type of case (single-user or multi-user). * @param caseDir The full path of the case directory. The directory * will be created if it doesn't already exist; if it @@ -1555,123 +1523,158 @@ public class Case { * @param caseNumber The case number, can be the empty string. * @param examiner The examiner to associate with the case, can be * the empty string. + */ + private Case(CaseType caseType, String caseDir, String caseDisplayName, String caseNumber, String examiner) { + metadata = new CaseMetadata(caseDir, caseType, displayNameToUniqueName(caseDisplayName), caseDisplayName, caseNumber, examiner); + } + + /** + * Constructs a Case object for an existing Autopsy case. * - * @throws CaseActionException if there is a problem creating the case. The + * @param caseMetaData The metadata for the case. + */ + private Case(CaseMetadata caseMetaData) { + metadata = caseMetaData; + } + + /** + * Opens this case by creating a task running in the same non-UI thread that + * will be used to close the case. If the case is a single-user case, this + * supports cancelling creation of the case by cancelling the task. If the + * case is a multi-user case, this ensures ensures that case directory lock + * held as long as the case is open is released in the same thread in which + * it was acquired, as is required by the coordination service. + * + * @param isNewCase True for a new case, false otherwise. + * + * @throws CaseActionException If there is a problem creating the case. The * exception will have a user-friendly message * and may be a wrapper for a lower-level * exception. */ @Messages({ "Case.progressIndicatorTitle.creatingCase=Creating Case", + "Case.progressIndicatorTitle.openingCase=Opening Case", "Case.progressIndicatorCancelButton.label=Cancel", "Case.progressMessage.preparing=Preparing...", - "Case.progressMessage.openingCaseResources=Preparing to open case resources.
This may take time if another user is upgrading the case." + "Case.progressMessage.preparingToOpenCaseResources=Preparing to open case resources.
This may take time if another user is upgrading the case.", + "Case.progressMessage.cancelling=Cancelling...", + "Case.exceptionMessage.cancelledByUser=Cancelled by user.", + "# {0} - exception message", "Case.exceptionMessage.execExceptionWrapperMessage={0}" }) - private void create(CaseType caseType, String caseDir, String caseDisplayName, String caseNumber, String examiner) throws CaseActionException { + private void open(boolean isNewCase) throws CaseActionException { /* - * Set up either a GUI progress indicator or a logging progress - * indicator. + * Create and start either a GUI progress indicator with a Cancel button + * or a logging progress indicator. */ - final CancelButtonListener listener = new CancelButtonListener(); + CancelButtonListener cancelButtonListener = null; ProgressIndicator progressIndicator; if (RuntimeProperties.runningWithGUI()) { + cancelButtonListener = new CancelButtonListener(Bundle.Case_progressMessage_cancelling()); + String progressIndicatorTitle = isNewCase ? Bundle.Case_progressIndicatorTitle_creatingCase() : Bundle.Case_progressIndicatorTitle_openingCase(); progressIndicator = new ModalDialogProgressIndicator( mainFrame, - Bundle.Case_progressIndicatorTitle_creatingCase(), + progressIndicatorTitle, new String[]{Bundle.Case_progressIndicatorCancelButton_label()}, Bundle.Case_progressIndicatorCancelButton_label(), - listener); + cancelButtonListener); } else { progressIndicator = new LoggingProgressIndicator(); } progressIndicator.start(Bundle.Case_progressMessage_preparing()); /* - * Creating a case is always done in the same non-UI thread that will be - * used later to close the case. If the case is a multi-user case, this - * ensures that case directory lock that is held as long as the case is - * open is released in the same thread in which it was acquired, as is - * required by the coordination service. + * Creating/opening a case is always done by creating a task running in + * the same non-UI thread that will be used to close the case, so a + * single-threaded executor service is created here and saved as case + * state (must be volatile for cancellation to work). + * + * --- If the case is a single-user case, this supports cancelling + * opening of the case by cancelling the task. + * + * --- If the case is a multi-user case, this still supports + * cancellation, but it also makes it possible for the shared case + * directory lock held as long as the case is open to be released in the + * same thread in which it was acquired, as is required by the + * coordination service. */ - caseLockingExecutor = Executors.newSingleThreadExecutor(); + TaskThreadFactory threadFactory = new TaskThreadFactory(String.format(CASE_ACTION_THREAD_NAME, metadata.getCaseName())); + caseLockingExecutor = Executors.newSingleThreadExecutor(threadFactory); Future future = caseLockingExecutor.submit(() -> { - if (CaseType.SINGLE_USER_CASE == caseType) { - create(caseType, caseDir, caseDisplayName, caseNumber, examiner, progressIndicator); + if (CaseType.SINGLE_USER_CASE == metadata.getCaseType()) { + open(isNewCase, progressIndicator); } else { /* - * Acquire a shared case directory lock that will be held as - * long as this node has this case open. This will prevent - * deletion of the case by another node. + * First, acquire a shared case directory lock that will be held + * as long as this node has this case open. This will prevent + * deletion of the case by another node. Next, acquire an + * exclusive case resources lock to ensure only one node at a + * time can create/open/upgrade/close the case resources. */ - progressIndicator.start(Bundle.Case_progressMessage_openingCaseResources()); - acquireSharedCaseDirLock(caseDir); - - /* - * Acquire an exclusive case resources lock to ensure only one - * node at a time can create/open/upgrade/close the case - * resources. - */ - try (CoordinationService.Lock resourcesLock = acquireExclusiveCaseResourcesLock(caseDir)) { + progressIndicator.progress(Bundle.Case_progressMessage_preparingToOpenCaseResources()); + acquireSharedCaseDirLock(metadata.getCaseDirectory()); + try (CoordinationService.Lock resourcesLock = acquireExclusiveCaseResourcesLock(metadata.getCaseDirectory())) { assert (null != resourcesLock); - try { - create(caseType, caseDir, caseDisplayName, caseNumber, examiner, progressIndicator); - } catch (CaseActionException ex) { - /* - * Release the case directory lock immediately if there - * was a problem opening the case. - */ - if (CaseType.MULTI_USER_CASE == caseType) { - releaseSharedCaseDirLock(caseDir); - } - throw ex; - } + open(isNewCase, progressIndicator); + } catch (CaseActionException ex) { + releaseSharedCaseDirLock(getMetadata().getCaseDirectory()); } } return null; }); - - /* - * If running with a GUI, give the future for the case creation task to - * the cancel button listener for the GUI progress indicator and make - * the progress indicator visible to the user. - */ - if (RuntimeProperties.runningWithGUI()) { - listener.setCaseActionFuture(future); - SwingUtilities.invokeLater(() -> ((ModalDialogProgressIndicator) progressIndicator).setVisible(true)); + if (null != cancelButtonListener) { + cancelButtonListener.setCaseActionFuture(future); } /* - * Wait for the case creation task to finish. + * Wait for the case creation/opening task to finish. */ try { future.get(); - } catch (InterruptedException ex) { - throw new CaseActionException(Bundle.Case_exceptionMessage_wrapperMessage(ex.getMessage()), ex); - } catch (ExecutionException ex) { - throw new CaseActionException(Bundle.Case_exceptionMessage_wrapperMessage(ex.getCause().getMessage()), ex); - } catch (CancellationException ex) { - throw new CaseActionException(Bundle.Case_exceptionMessage_cancelled(), ex); - } finally { - if (RuntimeProperties.runningWithGUI()) { - SwingUtilities.invokeLater(() -> ((ModalDialogProgressIndicator) progressIndicator).setVisible(false)); + } catch (InterruptedException discarded) { + /* + * The thread this method is running in has been interrupted. Cancel + * the create/open task, wait for it to finish, and shut down the + * executor. This can be done safely because if the task is + * completed with a cancellation condition, the case will have been + * closed and the case directory lock released will have been + * released. + */ + if (null != cancelButtonListener) { + cancelButtonListener.actionPerformed(null); + } else { + future.cancel(true); } + Case.shutDownTaskExecutor(caseLockingExecutor); + } catch (CancellationException discarded) { + /* + * The create/open task has been cancelled. Wait for it to finish, + * and shut down the executor. This can be done safely because if + * the task is completed with a cancellation condition, the case + * will have been closed and the case directory lock released will + * have been released. + */ + Case.shutDownTaskExecutor(caseLockingExecutor); + throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser()); + } catch (ExecutionException ex) { + /* + * The create/open task has thrown an exception. Wait for it to + * finish, and shut down the executor. This can be done safely + * because if the task is completed with an execution condition, the + * case will have been closed and the case directory lock released + * will have been released. + */ + Case.shutDownTaskExecutor(caseLockingExecutor); + throw new CaseActionException(Bundle.Case_exceptionMessage_execExceptionWrapperMessage(ex.getCause().getLocalizedMessage()), ex); + } finally { + progressIndicator.finish(); } } /** - * Creates a new case. + * Opens the case database and services for this case. * - * @param caseType The type of case (single-user or multi-user). - * @param caseDir The full path of the case directory. The - * directory will be created if it doesn't already - * exist; if it exists, it is ASSUMED it was - * created by calling createCaseDirectory. - * @param caseName The case name. - * @param caseDisplayName The display name of case, which may be changed - * later by the user. - * @param caseNumber The case number, can be the empty string. - * @param examiner The examiner to associate with the case, can be - * the empty string. + * @param isNewCase True for a new case, false otherwise. * @param progressIndicator A progress indicator. * * @throws CaseActionException if there is a problem creating the case. The @@ -1679,204 +1682,100 @@ public class Case { * and may be a wrapper for a lower-level * exception. */ + private void open(boolean isNewCase, ProgressIndicator progressIndicator) throws CaseActionException { + try { + if (Thread.currentThread().isInterrupted()) { + throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser()); + } + + if (isNewCase) { + createCaseData(progressIndicator); + } else { + openCaseData(progressIndicator); + } + + if (Thread.currentThread().isInterrupted()) { + throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser()); + } + + openServices(progressIndicator); + + if (Thread.currentThread().isInterrupted()) { + throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser()); + } + } catch (CaseActionException ex) { + /* + * Cancellation or failure. Clean up. The sleep is a little hack to + * clear the interrupted flag for this thread if this is a + * cancellation scenario, so that the clean up can run to completion + * in this thread. + */ + try { + Thread.sleep(1); + } catch (InterruptedException discarded) { + } + close(progressIndicator); + throw ex; + } + } + + /** + * Creates the case directory, case database, and case metadata file. + * + * @param progressIndicator A progress indicartor. + * + * @throws CaseActionException If there is a problem creating the case + * database. The exception will have a + * user-friendly message and may be a wrapper + * for a lower-level exception. + */ @Messages({ - "Case.exceptionMessage.emptyCaseName=Case name is empty.", "Case.progressMessage.creatingCaseDirectory=Creating case directory...", "Case.progressMessage.creatingCaseDatabase=Creating case database...", - "Case.exceptionMessage.couldNotCreateCaseDatabaseName=Failed to create case database name from case name.", - "Case.progressMessage.creatingCaseMetadataFile=Creating case metadata file...", - "Case.exceptionMessage.couldNotCreateMetadataFile=Failed to create case metadata file.", - "Case.exceptionMessage.couldNotCreateCaseDatabase=Failed to create case database." + "Case.exceptionMessage.couldNotCreateCaseDatabase=Failed to create case database.", + "Case.exceptionMessage.couldNotCreateMetadataFile=Failed to create case metadata file." }) - private void create(CaseType caseType, String caseDir, String caseDisplayName, String caseNumber, String examiner, ProgressIndicator progressIndicator) throws CaseActionException { - /* - * Create a unique and immutable case name from the case display name. - */ - if (caseDisplayName.isEmpty()) { - throw new CaseActionException(Bundle.Case_exceptionMessage_emptyCaseName()); - } - String caseName = displayNameToUniqueName(caseDisplayName); - + private void createCaseData(ProgressIndicator progressIndicator) throws CaseActionException { /* * Create the case directory, if it does not already exist. * - * TODO (JIRA-2180): The reason for this check for the existence of the - * case directory is not at all obvious. It reflects the assumption that - * if the case directory already exists, it is because the case is being - * created using the the "New Case" wizard, which separates the creation - * of the case directory from the creation of the case, with the idea - * that if the case directory cannot be created, the user can be asked - * to supply a different case directory path. This of course creates - * subtle and undetectable coupling between this code and the wizard - * code. The desired effect could be accomplished more simply and safely - * by having this method throw a specific exception to indicate that the - * case directory could not be created. In fact, a FEW specific - * exception types would in turn allow us to put localized, - * user-friendly messages in the GUI instead of putting user-friendly, - * localized messages in the exceptions, which causes them to appear in - * the application log, where it would be better to use English for - * readability by the broadest group of developers. + * TODO (JIRA-2180): Always create the case directory as part of the + * case creation process. */ - progressIndicator.progress(Bundle.Case_progressMessage_creatingCaseDirectory()); - if (new File(caseDir).exists() == false) { - Case.createCaseDirectory(caseDir, caseType); + if (new File(metadata.getCaseDirectory()).exists() == false) { + progressIndicator.progress(Bundle.Case_progressMessage_creatingCaseDirectory()); + Case.createCaseDirectory(metadata.getCaseDirectory(), metadata.getCaseType()); } /* * Create the case database. */ progressIndicator.progress(Bundle.Case_progressMessage_creatingCaseDatabase()); - String dbName = null; try { - if (CaseType.SINGLE_USER_CASE == caseType) { + if (CaseType.SINGLE_USER_CASE == metadata.getCaseType()) { /* * For single-user cases, the case database is a SQLite database * with a standard name, physically located in the root of the * case directory. */ - dbName = SINGLE_USER_CASE_DB_NAME; - this.caseDb = SleuthkitCase.newCase(Paths.get(caseDir, SINGLE_USER_CASE_DB_NAME).toString()); - } else if (CaseType.MULTI_USER_CASE == caseType) { + caseDb = SleuthkitCase.newCase(Paths.get(metadata.getCaseDirectory(), SINGLE_USER_CASE_DB_NAME).toString()); + metadata.setCaseDatabaseName(SINGLE_USER_CASE_DB_NAME); + } else { /* * For multi-user cases, the case database is a PostgreSQL * database with a name derived from the case display name, - * physically located on the database server. + * physically located on a database server. */ - this.caseDb = SleuthkitCase.newCase(caseDisplayName, UserPreferences.getDatabaseConnectionInfo(), caseDir); - dbName = this.caseDb.getDatabaseName(); + caseDb = SleuthkitCase.newCase(metadata.getCaseDisplayName(), UserPreferences.getDatabaseConnectionInfo(), metadata.getCaseDirectory()); + metadata.setCaseDatabaseName(caseDb.getDatabaseName()); } } catch (TskCoreException ex) { throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotCreateCaseDatabase(), ex); } catch (UserPreferencesException ex) { throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.databaseConnectionInfo.error.msg"), ex); - } - - /* - * Create the case metadata (.aut) file. - */ - progressIndicator.progress(Bundle.Case_progressMessage_creatingCaseMetadataFile()); - try { - this.caseMetadata = new CaseMetadata(caseDir, caseType, caseName, caseDisplayName, caseNumber, examiner, dbName); } catch (CaseMetadataException ex) { throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotCreateMetadataFile(), ex); } - - openServices(progressIndicator); - } - - /** - * Opens an existing case. - * - * @param caseMetadataFilePath The path to the case metadata file. - * - * @throws CaseActionException if there is a problem creating the case. The - * exception will have a user-friendly message - * and may be a wrapper for a lower-level - * exception. - */ - private void open(Path caseMetadataFilePath) throws CaseActionException { - /* - * Read the contents of the case metadata file. - */ - try { - caseMetadata = new CaseMetadata(caseMetadataFilePath); - } catch (CaseMetadataException ex) { - throw new CaseActionException(Bundle.Case_openException_couldNotOpenCase(Bundle.Case_exceptionMessage_failedToReadMetadata()), ex); - } - - /* - * Set up either a GUI progress indicator or a logging progress - * indicator. - */ - CancelButtonListener listener = new CancelButtonListener(); - ProgressIndicator progressIndicator; - if (RuntimeProperties.runningWithGUI()) { - progressIndicator = new ModalDialogProgressIndicator( - mainFrame, - Bundle.Case_progressIndicatorTitle_openingCase(), - new String[]{Bundle.Case_progressIndicatorCancelButton_label()}, - Bundle.Case_progressIndicatorCancelButton_label(), - listener); - } else { - progressIndicator = new LoggingProgressIndicator(); - } - progressIndicator.start(Bundle.Case_progressMessage_preparing()); - - /* - * Opening the case in the same thread that will be used later to close - * the case. If the case is a multi-user case, this ensures that case - * directory lock that is held as long as the case is open is released - * in the same thread in which it was acquired, as is required by the - * coordination service. - */ - CaseType caseType = caseMetadata.getCaseType(); - String caseName = caseMetadata.getCaseName(); - progressIndicator.start(Bundle.Case_progressMessage_preparing()); - caseLockingExecutor = Executors.newSingleThreadExecutor(); - Future future = caseLockingExecutor.submit(() -> { - if (CaseType.SINGLE_USER_CASE == caseType) { - openCaseDatabase(progressIndicator); - openServices(progressIndicator); - } else { - /* - * First, acquire a shared case directory lock that will be held - * as long as this node has this case open, in order to prevent - * deletion of the case by another node. - */ - progressIndicator.start(Bundle.Case_progressMessage_openingCaseResources()); - acquireSharedCaseDirLock(caseMetadata.getCaseDirectory()); - /* - * Next, acquire an exclusive case resources lock to ensure only - * one node at a time can create/open/upgrade/close case - * resources. - */ - try (CoordinationService.Lock resourcesLock = acquireExclusiveCaseResourcesLock(caseMetadata.getCaseDirectory())) { - assert (null != resourcesLock); - try { - openCaseDatabase(progressIndicator); - openServices(progressIndicator); - } catch (CaseActionException ex) { - /* - * Release the case directory lock immediately if there - * was a problem opening the case. - */ - if (CaseType.MULTI_USER_CASE == caseType) { - releaseSharedCaseDirLock(caseName); - } - throw ex; - } - } - } - return null; - }); - - /* - * If running with a GUI, give the future for the case opening task to - * the cancel button listener for the GUI progress indicator and make - * the progress indicator visible to the user. - */ - if (RuntimeProperties.runningWithGUI()) { - listener.setCaseActionFuture(future); - SwingUtilities.invokeLater(() -> ((ModalDialogProgressIndicator) progressIndicator).setVisible(true)); - } - - /* - * Wait for the case opening task to finish. - */ - try { - future.get(); - - } catch (InterruptedException ex) { - throw new CaseActionException(Bundle.Case_exceptionMessage_wrapperMessage(ex.getMessage()), ex); - } catch (ExecutionException ex) { - throw new CaseActionException(Bundle.Case_exceptionMessage_wrapperMessage(ex.getCause().getMessage()), ex); - } catch (CancellationException ex) { - throw new CaseActionException(Bundle.Case_exceptionMessage_cancelled(), ex); - } finally { - if (RuntimeProperties.runningWithGUI()) { - SwingUtilities.invokeLater(() -> ((ModalDialogProgressIndicator) progressIndicator).setVisible(false)); - } - } } /** @@ -1893,24 +1792,24 @@ public class Case { "Case.progressMessage.openingCaseDatabase=Opening case database...", "Case.exceptionMessage.couldNotOpenCaseDatabase=Failed to open case database." }) - private void openCaseDatabase(ProgressIndicator progressIndicator) throws CaseActionException { - /* - * Open the case database. - */ + private void openCaseData(ProgressIndicator progressIndicator) throws CaseActionException { try { progressIndicator.progress(Bundle.Case_progressMessage_openingCaseDatabase()); - if (CaseType.SINGLE_USER_CASE == caseMetadata.getCaseType()) { - this.caseDb = SleuthkitCase.openCase(Paths.get(caseMetadata.getCaseDirectory(), caseMetadata.getCaseDatabaseName()).toString()); + String databaseName = metadata.getCaseDatabaseName(); + if (CaseType.SINGLE_USER_CASE == metadata.getCaseType()) { + caseDb = SleuthkitCase.openCase(Paths.get(metadata.getCaseDirectory(), databaseName).toString()); } else if (UserPreferences.getIsMultiUserModeEnabled()) { try { - this.caseDb = SleuthkitCase.openCase(caseMetadata.getCaseDatabaseName(), UserPreferences.getDatabaseConnectionInfo(), caseMetadata.getCaseDirectory()); + caseDb = SleuthkitCase.openCase(databaseName, UserPreferences.getDatabaseConnectionInfo(), metadata.getCaseDirectory()); } catch (UserPreferencesException ex) { - throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.databaseConnectionInfo.error.msg"), ex); + throw new CaseActionException(NbBundle.getMessage(Case.class, + "Case.databaseConnectionInfo.error.msg"), ex); } } else { - throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.open.exception.multiUserCaseNotEnabled")); + throw new CaseActionException(NbBundle.getMessage(Case.class, + "Case.open.exception.multiUserCaseNotEnabled")); } } catch (TskCoreException ex) { throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotOpenCaseDatabase(), ex); @@ -1928,6 +1827,7 @@ public class Case { @Messages({ "Case.progressMessage.switchingLogDirectory=Switching log directory...", "Case.progressMessage.settingUpTskErrorReporting=Setting up SleuthKit error reporting...", + "Case.progressMessage.clearingTempDirectory=Clearing case temp directory...", "Case.progressMessage.openingCaseLevelServices=Opening case-level services...", "Case.progressMessage.openingApplicationServiceResources=Opening application service case resources...", "Case.progressMessage.settingUpNetworkCommunications=Setting up network communications...",}) @@ -1938,25 +1838,39 @@ public class Case { */ progressIndicator.progress(Bundle.Case_progressMessage_switchingLogDirectory()); Logger.setLogDirectory(getLogDirectoryPath()); + if (Thread.currentThread().isInterrupted()) { + throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser()); + } /* * Hook up a SleuthKit layer error reporter. */ progressIndicator.progress(Bundle.Case_progressMessage_settingUpTskErrorReporting()); - this.sleuthkitErrorReporter = new SleuthkitErrorReporter(MIN_SECS_BETWEEN_TSK_ERROR_REPORTS, NbBundle.getMessage(Case.class, "IntervalErrorReport.ErrorText")); - this.caseDb.addErrorObserver(this.sleuthkitErrorReporter); + sleuthkitErrorReporter + = new SleuthkitErrorReporter(MIN_SECS_BETWEEN_TSK_ERROR_REPORTS, NbBundle.getMessage(Case.class, + "IntervalErrorReport.ErrorText")); + caseDb.addErrorObserver(this.sleuthkitErrorReporter); + if (Thread.currentThread().isInterrupted()) { + throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser()); + } /* * Clear the temp subdirectory of the case directory. */ progressIndicator.progress(Bundle.Case_progressMessage_clearingTempDirectory()); Case.clearTempSubDir(this.getTempDirectory()); + if (Thread.currentThread().isInterrupted()) { + throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser()); + } /* * Open the case-level services. */ progressIndicator.progress(Bundle.Case_progressMessage_openingCaseLevelServices()); - this.caseServices = new Services(this.caseDb); + this.caseServices = new Services(caseDb); + if (Thread.currentThread().isInterrupted()) { + throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser()); + } /* * Allow any registered application services to open any resources @@ -1964,16 +1878,22 @@ public class Case { */ progressIndicator.progress(Bundle.Case_progressMessage_openingApplicationServiceResources()); openAppServiceCaseResources(); + if (Thread.currentThread().isInterrupted()) { + throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser()); + } /* * If this case is a multi-user case, set up for communication with * other nodes. */ - if (CaseType.MULTI_USER_CASE == this.caseMetadata.getCaseType()) { + if (CaseType.MULTI_USER_CASE == metadata.getCaseType()) { progressIndicator.progress(Bundle.Case_progressMessage_settingUpNetworkCommunications()); try { - eventPublisher.openRemoteEventChannel(String.format(EVENT_CHANNEL_NAME, this.getName())); - collaborationMonitor = new CollaborationMonitor(this.getName()); + eventPublisher.openRemoteEventChannel(String.format(EVENT_CHANNEL_NAME, metadata.getCaseName())); + if (Thread.currentThread().isInterrupted()) { + throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser()); + } + collaborationMonitor = new CollaborationMonitor(metadata.getCaseName()); } catch (AutopsyEventException | CollaborationMonitor.CollaborationMonitorException ex) { /* * The collaboration monitor and event channel are not @@ -1981,99 +1901,116 @@ public class Case { * throw. */ logger.log(Level.SEVERE, "Failed to setup network communications", ex); //NON-NLS - if (RuntimeProperties.runningWithGUI()) { - SwingUtilities.invokeLater(() -> MessageNotifyUtil.Notify.error(NbBundle.getMessage(Case.class, "Case.CollaborationSetup.FailNotify.Title"), NbBundle.getMessage(Case.class, "Case.CollaborationSetup.FailNotify.ErrMsg"))); + SwingUtilities.invokeLater(() -> MessageNotifyUtil.Notify.error( + NbBundle.getMessage(Case.class, "Case.CollaborationSetup.FailNotify.Title"), + NbBundle.getMessage(Case.class, "Case.CollaborationSetup.FailNotify.ErrMsg"))); } } } } /** - * Allows any registered application-level services to open any resources + * Allows any registered application-level services to open resources * specific to this case. - * - * @throws CaseActionException */ @NbBundle.Messages({ "# {0} - service name", "Case.serviceOpenCaseResourcesProgressIndicator.title={0} Opening Case Resources", - "# {0} - service name", "Case.servicesException.notificationTitle={0} Error", - "# {0} - service name", "Case.servicesException.serviceResourcesOpenCancelled=Opening case resources for {0} cancelled", - "# {0} - service name", "# {1} - exception message", "Case.servicesException.serviceResourcesOpenError=Could not open case resources for {0} service: {1}" + "# {0} - service name", "Case.serviceOpenCaseResourcesProgressIndicator.cancellingMessage=Cancelling opening case resources by {0}...", + "# {0} - service name", "Case.servicesException.notificationTitle={0} Error" }) private void openAppServiceCaseResources() throws CaseActionException { /* - * Each service gets its own independently cancellable task, and thus - * its own task progress indicator. + * Each service gets its own independently cancellable/interruptible + * task, running in a named thread managed by an executor service, with + * its own progress indicator. This allows for cancellation of the + * opening of case resources for individual services. It also makes it + * possible to ensure that each service task completes before the next + * one starts by awaiting termination of the executor service. */ - ExecutorService executor = Executors.newSingleThreadExecutor(); - for (AutopsyService service : Lookup.getDefault().lookupAll(AutopsyService.class)) { - CancelButtonListener buttonListener = new CancelButtonListener(); + /* + * Create a progress indicator for the task and start the task. If + * running with a GUI, the progress indicator will be a dialog box + * with a Cancel button. + */ + CancelButtonListener cancelButtonListener = null; ProgressIndicator progressIndicator; if (RuntimeProperties.runningWithGUI()) { + cancelButtonListener = new CancelButtonListener(Bundle.Case_serviceOpenCaseResourcesProgressIndicator_cancellingMessage(service.getServiceName())); progressIndicator = new ModalDialogProgressIndicator( mainFrame, Bundle.Case_serviceOpenCaseResourcesProgressIndicator_title(service.getServiceName()), new String[]{Bundle.Case_progressIndicatorCancelButton_label()}, Bundle.Case_progressIndicatorCancelButton_label(), - buttonListener); + cancelButtonListener); } else { progressIndicator = new LoggingProgressIndicator(); } progressIndicator.start(Bundle.Case_progressMessage_preparing()); - AutopsyService.CaseContext context = new AutopsyService.CaseContext(this, progressIndicator); - if (RuntimeProperties.runningWithGUI()) { - buttonListener.setCaseContext(context); - } + String threadNameSuffix = service.getServiceName().replaceAll("[ ]", "-"); //NON-NLS + threadNameSuffix = threadNameSuffix.toLowerCase(); + TaskThreadFactory threadFactory = new TaskThreadFactory(String.format(CASE_RESOURCES_THREAD_NAME, threadNameSuffix)); + ExecutorService executor = Executors.newSingleThreadExecutor(threadFactory); Future future = executor.submit(() -> { service.openCaseResources(context); return null; }); - if (RuntimeProperties.runningWithGUI()) { - buttonListener.setCaseActionFuture(future); - SwingUtilities.invokeLater(() -> ((ModalDialogProgressIndicator) progressIndicator).setVisible(true)); + if (null != cancelButtonListener) { + cancelButtonListener.setCaseContext(context); + cancelButtonListener.setCaseActionFuture(future); } + + /* + * Wait for the task to either be completed or + * cancelled/interrupted, or for the opening of the case to be + * cancelled. + */ try { future.get(); - } catch (InterruptedException ex) { - Case.logger.log(Level.SEVERE, String.format("Unexpected interrupt while waiting on %s service to open case resources", service.getServiceName()), ex); - - } catch (CancellationException ex) { + } catch (InterruptedException discarded) { /* - * The case-specific application service resources are not - * essential. Log an error and notify the user if running the - * desktop GUI, but do not throw. + * The parent create/open case task has been cancelled. */ - Case.logger.log(Level.WARNING, String.format("%s service opening of case resources cancelled", service.getServiceName())); - if (RuntimeProperties.runningWithGUI()) { - SwingUtilities.invokeLater(() -> MessageNotifyUtil.Notify.warn( - Bundle.Case_servicesException_notificationTitle(service.getServiceName()), - Bundle.Case_servicesException_serviceResourcesOpenCancelled(service.getServiceName()))); - } + Case.logger.log(Level.WARNING, String.format("Opening of %s (%s) in %s cancelled during opening of case resources by %s", getDisplayName(), getName(), getCaseDirectory(), service.getServiceName())); + future.cancel(true); + } catch (CancellationException discarded) { + /* + * The opening of case resources by the application service has + * been cancelled, so the executor service has thrown. Note that + * there is no guarantee the task itself has responded to the + * cancellation request yet. + */ + Case.logger.log(Level.WARNING, String.format("Opening of case resources by %s for %s (%s) in %s cancelled", service.getServiceName(), getDisplayName(), getName(), getCaseDirectory(), service.getServiceName())); } catch (ExecutionException ex) { /* - * The case-specific application service resources are not + * An exception was thrown while executing the task. The + * case-specific application service resources are not * essential. Log an error and notify the user if running the * desktop GUI, but do not throw. */ - Case.logger.log(Level.SEVERE, String.format("%s service failed to open case resources", service.getServiceName()), ex); + Case.logger.log(Level.SEVERE, String.format("%s failed to open case resources for %s", service.getServiceName(), this.getDisplayName()), ex); if (RuntimeProperties.runningWithGUI()) { - SwingUtilities.invokeLater(() -> MessageNotifyUtil.Notify.error( - Bundle.Case_servicesException_notificationTitle(service.getServiceName()), - Bundle.Case_servicesException_serviceResourcesOpenError(service.getServiceName(), ex.getLocalizedMessage()))); + SwingUtilities.invokeLater(() -> { + MessageNotifyUtil.Notify.error(Bundle.Case_servicesException_notificationTitle(service.getServiceName()), ex.getLocalizedMessage()); + }); } } finally { - if (RuntimeProperties.runningWithGUI()) { - SwingUtilities.invokeLater(() -> ((ModalDialogProgressIndicator) progressIndicator).setVisible(false)); - } + /* + * Shut down the executor service and wait for it to finish. + * This ensures that the task has finished. Without this, it + * would be possible to start the next task before the current + * task responded to a cancellation request. + */ + shutDownTaskExecutor(executor); + progressIndicator.finish(); + } + + if (Thread.currentThread().isInterrupted()) { + return; } } - /* - * No tasks left, simply shut down the executor. - */ - executor.shutdown(); } /** @@ -2081,25 +2018,15 @@ public class Case { * * @param progressIndicator A progress indicator. */ - @Messages({ - "Case.progressMessage.closingCaseResources=Preparing to close case resources.
This may take time if another user is upgrading the case.", - "Case.progressMessage.notifyingCaseEventSubscribers=Notifying case event subscribers...", - "Case.progressMessage.clearingTempDirectory=Clearing case temp directory...", - "Case.progressMessage.closingCaseLevelServices=Closing case-level services...", - "Case.progressMessage.closingApplicationServiceResources=Closing case-specific application service resources...", - "Case.progressMessage.tearingDownNetworkCommunications=Tearing down network communications...", - "Case.progressMessage.closingCaseDatabase=Closing case database...", - "Case.progressMessage.tearingDownTskErrorReporting=Tearing down SleuthKit error reporting..." - }) private void close() throws CaseActionException { /* - * Set up either a GUI progress indicator or a logging progress - * indicator. + * Set up either a GUI progress indicator without a Cancel button or a + * logging progress indicator. */ ProgressIndicator progressIndicator; if (RuntimeProperties.runningWithGUI()) { progressIndicator = new ModalDialogProgressIndicator( - Case.mainFrame, + mainFrame, Bundle.Case_progressIndicatorTitle_closingCase()); } else { progressIndicator = new LoggingProgressIndicator(); @@ -2114,17 +2041,16 @@ public class Case { * required by the coordination service. */ Future future = caseLockingExecutor.submit(() -> { - if (CaseType.SINGLE_USER_CASE == caseMetadata.getCaseType()) { + if (CaseType.SINGLE_USER_CASE == metadata.getCaseType()) { close(progressIndicator); } else { - String caseName = caseMetadata.getCaseName(); /* * Acquire an exclusive case resources lock to ensure only one * node at a time can create/open/upgrade/close the case * resources. */ - progressIndicator.start(Bundle.Case_progressMessage_closingCaseResources()); - try (CoordinationService.Lock resourcesLock = acquireExclusiveCaseResourcesLock(caseMetadata.getCaseDirectory())) { + progressIndicator.progress(Bundle.Case_progressMessage_preparing()); + try (CoordinationService.Lock resourcesLock = acquireExclusiveCaseResourcesLock(metadata.getCaseDirectory())) { assert (null != resourcesLock); close(progressIndicator); } finally { @@ -2132,38 +2058,40 @@ public class Case { * Always release the case directory lock that was acquired * when the case was opened. */ - releaseSharedCaseDirLock(caseName); + releaseSharedCaseDirLock(metadata.getCaseName()); } } return null; }); - /* - * If running with a GUI, give the future for the case closing task to - * the cancel button listener for the GUI progress indicator and make - * the progress indicator visible to the user. - */ - if (RuntimeProperties.runningWithGUI()) { - SwingUtilities.invokeLater(() -> ((ModalDialogProgressIndicator) progressIndicator).setVisible(true)); - } - try { - logger.log(Level.INFO, "Closing case with metadata file path {0}", getCaseMetadata().getFilePath()); //NON-NLS future.get(); - } catch (InterruptedException ex) { - throw new CaseActionException(Bundle.Case_exceptionMessage_wrapperMessage(ex.getMessage()), ex); + } catch (InterruptedException | CancellationException unused) { + /* + * The wait has been interrupted by interrupting the thread running + * this method. Not allowing cancellation of case closing, so ignore + * the interrupt. Likewsie, cancellation of the case closing task is + * not supported. + */ } catch (ExecutionException ex) { - throw new CaseActionException(Bundle.Case_exceptionMessage_wrapperMessage(ex.getCause().getMessage()), ex); - } catch (CancellationException ex) { - throw new CaseActionException(Bundle.Case_exceptionMessage_cancelled(), ex); + throw new CaseActionException(Bundle.Case_exceptionMessage_execExceptionWrapperMessage(ex.getCause().getMessage()), ex); } finally { - caseLockingExecutor.shutdown(); - if (RuntimeProperties.runningWithGUI()) { - SwingUtilities.invokeLater(() -> ((ModalDialogProgressIndicator) progressIndicator).setVisible(false)); - } + shutDownTaskExecutor(caseLockingExecutor); + progressIndicator.finish(); } } + /** + * Closes the case. + * + * @param progressIndicator A progress indicator. + */ + @Messages({ + "Case.progressMessage.shuttingDownNetworkCommunications=Shutting down network communications...", + "Case.progressMessage.closingApplicationServiceResources=Closing case-specific application service resources...", + "Case.progressMessage.closingCaseLevelServices=Closing case-level services...", + "Case.progressMessage.closingCaseDatabase=Closing case database...", + "Case.progressMessage.shuttingDownTskErrorReporting=Shutting down SleuthKit error reporting..."}) private void close(ProgressIndicator progressIndicator) { IngestManager.getInstance().cancelAllIngestJobs(IngestJob.CancellationReason.CASE_CLOSED); @@ -2171,8 +2099,8 @@ public class Case { * Stop sending/receiving case events to and from other nodes if this is * a multi-user case. */ - if (CaseType.MULTI_USER_CASE == caseMetadata.getCaseType()) { - progressIndicator.progress(Bundle.Case_progressMessage_tearingDownNetworkCommunications()); + if (CaseType.MULTI_USER_CASE == metadata.getCaseType()) { + progressIndicator.progress(Bundle.Case_progressMessage_shuttingDownNetworkCommunications()); if (null != collaborationMonitor) { collaborationMonitor.shutdown(); } @@ -2189,24 +2117,26 @@ public class Case { /* * Close the case-level services. */ - progressIndicator.progress(Bundle.Case_progressMessage_closingCaseLevelServices()); - try { - this.caseServices.close(); - } catch (IOException ex) { - logger.log(Level.SEVERE, String.format("Error closing internal case services for %s at %s", this.getName(), this.getCaseDirectory()), ex); + if (null != caseServices) { + progressIndicator.progress(Bundle.Case_progressMessage_closingCaseLevelServices()); + try { + this.caseServices.close(); + } catch (IOException ex) { + logger.log(Level.SEVERE, String.format("Error closing internal case services for %s at %s", this.getName(), this.getCaseDirectory()), ex); + } } /* * Close the case database */ - progressIndicator.progress(Bundle.Case_progressMessage_closingCaseDatabase()); - caseDb.close(); - - /* - * Disconnect the SleuthKit layer error reporter. - */ - progressIndicator.progress(Bundle.Case_progressMessage_tearingDownTskErrorReporting()); - caseDb.removeErrorObserver(sleuthkitErrorReporter); + if (null != caseDb) { + progressIndicator.progress(Bundle.Case_progressMessage_closingCaseDatabase()); + caseDb.close(); + if (null != sleuthkitErrorReporter) { + progressIndicator.progress(Bundle.Case_progressMessage_shuttingDownTskErrorReporting()); + caseDb.removeErrorObserver(sleuthkitErrorReporter); + } + } /* * Switch the log directory. @@ -2228,10 +2158,7 @@ public class Case { * Each service gets its own independently cancellable task, and thus * its own task progress indicator. */ - ExecutorService executor = Executors.newSingleThreadExecutor(); - - for (AutopsyService service : Lookup.getDefault().lookupAll(AutopsyService.class - )) { + for (AutopsyService service : Lookup.getDefault().lookupAll(AutopsyService.class)) { ProgressIndicator progressIndicator; if (RuntimeProperties.runningWithGUI()) { progressIndicator = new ModalDialogProgressIndicator( @@ -2241,23 +2168,21 @@ public class Case { progressIndicator = new LoggingProgressIndicator(); } progressIndicator.start(Bundle.Case_progressMessage_preparing()); - AutopsyService.CaseContext context = new AutopsyService.CaseContext(this, progressIndicator); + String threadNameSuffix = service.getServiceName().replaceAll("[ ]", "-"); //NON-NLS + threadNameSuffix = threadNameSuffix.toLowerCase(); + TaskThreadFactory threadFactory = new TaskThreadFactory(String.format(CASE_RESOURCES_THREAD_NAME, threadNameSuffix)); + ExecutorService executor = Executors.newSingleThreadExecutor(threadFactory); Future future = executor.submit(() -> { service.closeCaseResources(context); return null; }); - if (RuntimeProperties.runningWithGUI()) { - SwingUtilities.invokeLater(() -> ((ModalDialogProgressIndicator) progressIndicator).setVisible(true)); - } try { future.get(); } catch (InterruptedException ex) { Case.logger.log(Level.SEVERE, String.format("Unexpected interrupt while waiting on %s service to close case resources", service.getServiceName()), ex); - } catch (CancellationException ex) { Case.logger.log(Level.SEVERE, String.format("Unexpected cancellation while waiting on %s service to close case resources", service.getServiceName()), ex); - } catch (ExecutionException ex) { Case.logger.log(Level.SEVERE, String.format("%s service failed to open case resources", service.getServiceName()), ex); if (RuntimeProperties.runningWithGUI()) { @@ -2266,15 +2191,10 @@ public class Case { Bundle.Case_servicesException_serviceResourcesCloseError(service.getServiceName(), ex.getLocalizedMessage()))); } } finally { - if (RuntimeProperties.runningWithGUI()) { - SwingUtilities.invokeLater(() -> ((ModalDialogProgressIndicator) progressIndicator).setVisible(false)); - } + shutDownTaskExecutor(executor); + progressIndicator.finish(); } } - /* - * No tasks left, simply shut down the executor. - */ - executor.shutdown(); } /** @@ -2328,28 +2248,159 @@ public class Case { } + /** + * Shuts down a task executor service, waiting until all tasks are + * terminated. The current policy is to wait for the tasks to finish so that + * the case for which the executor is running can be left in a consistent + * state. + * + * @param executor The executor. + */ + private static void shutDownTaskExecutor(ExecutorService executor) { + executor.shutdown(); + boolean taskCompleted = false; + while (!taskCompleted) { + try { + taskCompleted = executor.awaitTermination(EXECUTOR_AWAIT_TIMEOUT_SECS, TimeUnit.SECONDS); + } catch (InterruptedException ignored) { + /* + * The current policy is to wait for the task to finish so that + * the case can be left in a consistent state. + * + * For a specific example of the motivation for this policy, + * note that a application service (Solr search service) + * experienced an error condition when opening case resources + * that left the service blocked uninterruptibly on a socket + * read. This eventually led to a mysterious "freeze" as the + * user-cancelled service task continued to run holdiong a lock + * that a UI thread soon tried to acquire. Thus it has been + * deemed better to make the "freeze" happen in a more + * informative way, i.e., with the progress indicator for the + * unfinished task on the screen, if a similar error condition + * arises again. + */ + } + } + } + + /** + * A case operation Cancel button listener for use with a + * ModalDialogProgressIndicator when running with a GUI. + */ + @ThreadSafe private final static class CancelButtonListener implements ActionListener { - private Future caseActionFuture; + private final String cancellationMessage; + @GuardedBy("this") + private boolean cancelRequested; + @GuardedBy("this") private CaseContext caseContext; + @GuardedBy("this") + private Future caseActionFuture; - private void setCaseActionFuture(Future caseActionFuture) { - this.caseActionFuture = caseActionFuture; + /** + * Constructs a case operation Cancel button listener for use with a + * ModalDialogProgressIndicator when running with a GUI. + * + * @param cancellationMessage The message to display in the + * ModalDialogProgressIndicator when the + * cancel button is pressed. + */ + private CancelButtonListener(String cancellationMessage) { + this.cancellationMessage = cancellationMessage; } - private void setCaseContext(CaseContext caseContext) { + /** + * Sets a case context for this listener. + * + * @param caseContext A case context object. + */ + private synchronized void setCaseContext(CaseContext caseContext) { this.caseContext = caseContext; + /* + * If the cancel button has already been pressed, pass the + * cancellation on to the case context. + */ + if (cancelRequested) { + cancel(); + } } + /** + * Sets a Future object for a task associated with this listener. + * + * @param caseActionFuture A task Future object. + */ + private synchronized void setCaseActionFuture(Future caseActionFuture) { + this.caseActionFuture = caseActionFuture; + /* + * If the cancel button has already been pressed, cancel the Future + * of the task. + */ + if (cancelRequested) { + cancel(); + } + } + + /** + * The event handler for Cancel button pushes. + * + * @param event The button event, ignored, can be null. + */ @Override - public void actionPerformed(ActionEvent e) { + public synchronized void actionPerformed(ActionEvent event) { + cancel(); + } + + /** + * Handles a cancellation request. + */ + private void cancel() { + /* + * At a minimum, set the cancellation requested flag of this + * listener. + */ + this.cancelRequested = true; if (null != this.caseContext) { + /* + * Set the cancellation request flag and display the + * cancellation message in the progress indicator for the case + * context associated with this listener. + */ + if (RuntimeProperties.runningWithGUI()) { + ProgressIndicator progressIndicator = this.caseContext.getProgressIndicator(); + if (progressIndicator instanceof ModalDialogProgressIndicator) { + ((ModalDialogProgressIndicator) progressIndicator).setCancelling(cancellationMessage); + } + } this.caseContext.requestCancel(); } if (null != this.caseActionFuture) { + /* + * Cancel the Future of the task associated with this listener. + * Note that the task thread will be interrupted if the task is + * blocked. + */ this.caseActionFuture.cancel(true); } } + } + + /** + * A thread factory that provides named threads. + */ + private static class TaskThreadFactory implements ThreadFactory { + + private final String threadName; + + private TaskThreadFactory(String threadName) { + this.threadName = threadName; + } + + @Override + public Thread newThread(Runnable task) { + return new Thread(task, threadName); + } } @@ -2594,4 +2645,16 @@ public class Case { deleteReports(reports); } + /** + * Sets the name of the keyword search index for the case. + * + * @param textIndexName The text index name. + * + * @throws CaseMetadataException + * @dprecated Do not use. + */ + @Deprecated + public void setTextIndexName(String textIndexName) throws CaseMetadataException { + } + } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseActionCancelledException.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseActionCancelledException.java new file mode 100755 index 0000000000..042bfe912e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseActionCancelledException.java @@ -0,0 +1,49 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-2017 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.casemodule; + +/** + * Exception thrown when a case action (e.g., create, open, close, delete) is + * cancelled before it is completed. + */ +class CaseActionCancelledException extends CaseActionException { + + private static final long serialVersionUID = 1L; + + /** + * Constructs an exception thrown when a case action (e.g., create, open, + * close, delete) is cancelled before it is completed. + * + * @param message An error message. + */ + CaseActionCancelledException(String message) { + super(message); + } + + /** + * Constructs an exception thrown when a case action (e.g., create, open, + * close, delete) is cancelled before it is completed. + * + * @param message An error message. + * @param cause An excception that caused this exception to be thrown. + */ + CaseActionCancelledException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseActionException.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseActionException.java index 01bad4a628..3cbcc6fedf 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseActionException.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseActionException.java @@ -1,15 +1,15 @@ /* * Autopsy Forensic Browser - * - * Copyright 2012 Basis Technology Corp. + * + * Copyright 2011-2017 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. @@ -19,15 +19,30 @@ package org.sleuthkit.autopsy.casemodule; /** - * Exception thrown when case action (such as open, close, create) resulted in - * an error + * Exception thrown when a case action (e.g., create, open, close, delete) + * experiences an error condition. */ public class CaseActionException extends Exception { + private static final long serialVersionUID = 1L; + + /** + * Constructs an exception thrown when a case action (e.g., create, open, + * close, delete) experiences an error condition. + * + * @param message An error message. + */ public CaseActionException(String message) { super(message); } + /** + * Constructs an exception thrown when a case action (e.g., create, open, + * close, delete) experiences an error condition. + * + * @param message An error message. + * @param cause An excception that caused this exception to be thrown. + */ public CaseActionException(String message, Throwable cause) { super(message, cause); } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseInformationPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseInformationPanel.java index cc81d70a30..b51f0d8a63 100755 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseInformationPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseInformationPanel.java @@ -1,15 +1,15 @@ /* * Autopsy Forensic Browser - * - * Copyright 2011-2016 Basis Technology Corp. + * + * Copyright 2011-2017 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. @@ -19,10 +19,6 @@ package org.sleuthkit.autopsy.casemodule; import java.awt.event.ActionListener; -import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; -import javax.swing.JOptionPane; import javax.swing.event.ChangeEvent; import org.openide.util.NbBundle.Messages; @@ -32,44 +28,40 @@ import org.openide.util.NbBundle.Messages; */ class CaseInformationPanel extends javax.swing.JPanel { - private static final Logger logger = Logger.getLogger(CaseInformationPanel.class.getName()); + private static final long serialVersionUID = 1L; /** - * Creates new form CaseInformationPanel + * Constructs a panel for displaying the case information, including both + * case details and ingest job history. */ CaseInformationPanel() { initComponents(); customizeComponents(); } - @Messages({"CaseInformationPanel.caseDetails.header=Case Details", - "CaseInformationPanel.ingestJobInfo.header=Ingest History", - "CaseInformationPanel.loadMetadataFail.message=Failed to load case metadata.", - "CaseInformationPanel.loadMetadataFail.title=Metadata load failure",}) + @Messages({ + "CaseInformationPanel.caseDetails.header=Case Details", + "CaseInformationPanel.ingestJobInfo.header=Ingest History" + }) private void customizeComponents() { - try { - Case currentCase = Case.getCurrentCase(); - String crDate = currentCase.getCreatedDate(); - String caseDir = currentCase.getCaseDirectory(); - - // put the image paths information into hashmap - Map imgPaths = Case.getImagePaths(currentCase.getSleuthkitCase()); - CasePropertiesPanel cpf = new CasePropertiesPanel(currentCase, crDate, caseDir, imgPaths); - cpf.setSize(cpf.getPreferredSize()); - this.tabbedPane.addTab(Bundle.CaseInformationPanel_caseDetails_header(), cpf); - this.tabbedPane.addTab(Bundle.CaseInformationPanel_ingestJobInfo_header(), new IngestJobInfoPanel()); - this.tabbedPane.addChangeListener((ChangeEvent e) -> { - tabbedPane.getSelectedComponent().setSize(tabbedPane.getSelectedComponent().getPreferredSize()); - }); - } catch (CaseMetadata.CaseMetadataException ex) { - logger.log(Level.SEVERE, "Failed to load case metadata.", ex); - JOptionPane.showMessageDialog(null, Bundle.IngestJobInfoPanel_loadIngestJob_error_text(), Bundle.IngestJobInfoPanel_loadIngestJob_error_title(), JOptionPane.ERROR_MESSAGE); - } + CasePropertiesPanel propertiesPanel = new CasePropertiesPanel(Case.getCurrentCase()); + propertiesPanel.setSize(propertiesPanel.getPreferredSize()); + this.tabbedPane.addTab(Bundle.CaseInformationPanel_caseDetails_header(), propertiesPanel); + this.tabbedPane.addTab(Bundle.CaseInformationPanel_ingestJobInfo_header(), new IngestJobInfoPanel()); + this.tabbedPane.addChangeListener((ChangeEvent e) -> { + tabbedPane.getSelectedComponent().setSize(tabbedPane.getSelectedComponent().getPreferredSize()); + }); } - + + /** + * Adds an action listener to the Close button of the panel. + * + * @param action + */ void addCloseButtonAction(ActionListener action) { this.closeButton.addActionListener(action); } + /** * 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 @@ -127,7 +119,7 @@ class CaseInformationPanel extends javax.swing.JPanel { }// //GEN-END:initComponents private void closeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_closeButtonActionPerformed - // TODO add your handling code here: + // Used by CasePropertiesAction }//GEN-LAST:event_closeButtonActionPerformed diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java index 9a870e25a2..1f4c5b7912 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java @@ -54,7 +54,9 @@ public final class CaseMetadata { private static final String FILE_EXTENSION = ".aut"; private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss (z)"); - //fields from schema version 1 + /* + * Fields from schema version 1 + */ private static final String SCHEMA_VERSION_ONE = "1.0"; private final static String ROOT_ELEMENT_NAME = "AutopsyCase"; //NON-NLS private final static String SCHEMA_VERSION_ELEMENT_NAME = "SchemaVersion"; //NON-NLS @@ -68,22 +70,28 @@ public final class CaseMetadata { private final static String CASE_DATABASE_NAME_ELEMENT_NAME = "DatabaseName"; //NON-NLS private final static String TEXT_INDEX_NAME_ELEMENT = "TextIndexName"; //NON-NLS - //fields from schema version 2 + /* + * Fields from schema version 2 + */ private static final String SCHEMA_VERSION_TWO = "2.0"; private final static String AUTOPSY_CREATED_BY_ELEMENT_NAME = "CreatedByAutopsyVersion"; //NON-NLS - private final static String CASE_DATABASE_ABSOLUTE_PATH_ELEMENT_NAME = "Database"; //NON-NLS + private final static String CASE_DB_ABSOLUTE_PATH_ELEMENT_NAME = "Database"; //NON-NLS private final static String TEXT_INDEX_ELEMENT = "TextIndex"; //NON-NLS - //fields from schema version 3 + /* + * Fields from schema version 3 + */ private static final String SCHEMA_VERSION_THREE = "3.0"; private final static String CASE_DISPLAY_NAME_ELEMENT_NAME = "DisplayName"; //NON-NLS - private final static String CASE_DATABASE_NAME_RELATIVE_ELEMENT_NAME = "CaseDatabase"; //NON-NLS + private final static String CASE_DB_NAME_RELATIVE_ELEMENT_NAME = "CaseDatabase"; //NON-NLS - //unread fields, these are regenerated on save + /* + * Unread fields, regenerated on save. + */ private final static String MODIFIED_DATE_ELEMENT_NAME = "ModifiedDate"; //NON-NLS private final static String AUTOPSY_SAVED_BY_ELEMENT_NAME = "SavedByAutopsyVersion"; //NON-NLS - private static final String CURRENT_SCHEMA_VERSION = SCHEMA_VERSION_THREE; + private final static String CURRENT_SCHEMA_VERSION = SCHEMA_VERSION_THREE; private final Path metadataFilePath; private Case.CaseType caseType; @@ -92,8 +100,8 @@ public final class CaseMetadata { private String caseNumber; private String examiner; private String caseDatabaseName; - private String caseDatabasePath; - private String textIndexName; + private String caseDatabasePath; // Legacy + private String textIndexName; // Legacy private String createdDate; private String createdByVersion; @@ -107,8 +115,9 @@ public final class CaseMetadata { } /** - * Constructs an object that provides access to the case metadata stored in - * a new case metadata file that is created using the supplied metadata. + * Constructs a CaseMetadata object for a new case. The metadata is not + * persisted to the case metadata file until writeFile or a setX method is + * called. * * @param caseDirectory The case directory. * @param caseType The type of case. @@ -117,29 +126,24 @@ public final class CaseMetadata { * user. * @param caseNumber The case number. * @param examiner The name of the case examiner. - * @param caseDatabase For a single-user case, the full path to the case - * database file. For a multi-user case, the case - * database name. - * - * @throws CaseMetadataException If the new case metadata file cannot be - * created. */ - CaseMetadata(String caseDirectory, Case.CaseType caseType, String caseName, String caseDisplayName, String caseNumber, String examiner, String caseDatabase) throws CaseMetadataException { + CaseMetadata(String caseDirectory, Case.CaseType caseType, String caseName, String caseDisplayName, String caseNumber, String examiner) { metadataFilePath = Paths.get(caseDirectory, caseDisplayName + FILE_EXTENSION); this.caseType = caseType; this.caseName = caseName; this.caseDisplayName = caseDisplayName; this.caseNumber = caseNumber; this.examiner = examiner; - this.caseDatabaseName = caseDatabase; + caseDatabaseName = ""; + caseDatabasePath = ""; + textIndexName = ""; createdByVersion = Version.getVersion(); createdDate = CaseMetadata.DATE_FORMAT.format(new Date()); - writeToFile(); } /** - * Constructs an object that provides access to the case metadata stored in - * an existing case metadata file. + * Constructs a CaseMetadata object for an existing case. The metadata is + * read from an existing case metadata file. * * @param metadataFilePath The full path to the case metadata file. * @@ -179,7 +183,7 @@ public final class CaseMetadata { } /** - * Gets the immutable case name, set at case creation. + * Gets the unique and immutable case name. * * @return The case display name. */ @@ -193,22 +197,23 @@ public final class CaseMetadata { * @return The case display name. */ public String getCaseDisplayName() { - return this.caseDisplayName; + return caseDisplayName; } /** - * Sets the case display name. This does not change the name of the case - * directory, the case database, or the text index name. + * Sets the case display name. * - * @param caseName A case display name. + * @param caseDisplayName A case display name. + * + * @throws CaseMetadataException If the operation fails. */ - void setCaseDisplayName(String caseName) throws CaseMetadataException { - String oldCaseName = caseName; - this.caseDisplayName = caseName; + void setCaseDisplayName(String caseDisplayName) throws CaseMetadataException { + String oldCaseDisplayName = this.caseDisplayName; + this.caseDisplayName = caseDisplayName; try { writeToFile(); } catch (CaseMetadataException ex) { - this.caseDisplayName = oldCaseName; + this.caseDisplayName = oldCaseDisplayName; throw ex; } } @@ -234,32 +239,35 @@ public final class CaseMetadata { /** * Gets the name of the case database. * - * @return The case database name. + * @return The case database name, may be empty. */ public String getCaseDatabaseName() { return caseDatabaseName; } /** - * Sets the text index name. + * Sets the name of the case database. * - * @param caseTextIndexName The text index name. + * @param caseDatabaseName The case database name. + * + * @throws CaseMetadataException If the operation fails. */ - void setTextIndexName(String caseTextIndexName) throws CaseMetadataException { - String oldIndexName = caseTextIndexName; - this.textIndexName = caseTextIndexName; + void setCaseDatabaseName(String caseDatabaseName) throws CaseMetadataException { + String oldCaseDatabaseName = this.caseDatabaseName; + this.caseDatabaseName = caseDatabaseName; try { writeToFile(); } catch (CaseMetadataException ex) { - this.textIndexName = oldIndexName; + this.caseDatabaseName = oldCaseDatabaseName; throw ex; } } /** - * Gets the text index name. + * Gets the text index name. This is a legacy field and will be empty for + * cases created with Autopsy 4.4.0 and above. * - * @return The name of the text index for the case. + * @return The name of the text index for the case, may be empty. */ public String getTextIndexName() { return textIndexName; @@ -268,7 +276,7 @@ public final class CaseMetadata { /** * Gets the date the case was created. * - * @return The date this case was created as a string + * @return The date this case was created, as a string. */ String getCreatedDate() { return createdDate; @@ -278,7 +286,9 @@ public final class CaseMetadata { * Sets the date the case was created. Used for preserving the case creation * date during single-user to multi-user case conversion. * - * @param createdDate The date the case was created as a string. + * @param createdDate The date the case was created, as a string. + * + * @throws CaseMetadataException If the operation fails. */ void setCreatedDate(String createdDate) throws CaseMetadataException { String oldCreatedDate = createdDate; @@ -304,13 +314,15 @@ public final class CaseMetadata { * Sets the Autopsy version that created the case. Used for preserving this * metadata during single-user to multi-user case conversion. * - * @param buildVersion An build version identifier. + * @param buildVersion A build version identifier. + * + * @throws CaseMetadataException If the operation fails. */ void setCreatedByVersion(String buildVersion) throws CaseMetadataException { String oldCreatedByVersion = this.createdByVersion; this.createdByVersion = buildVersion; try { - this.writeToFile(); + writeToFile(); } catch (CaseMetadataException ex) { this.createdByVersion = oldCreatedByVersion; throw ex; @@ -381,8 +393,8 @@ public final class CaseMetadata { createChildElement(doc, caseElement, CASE_NUMBER_ELEMENT_NAME, caseNumber); createChildElement(doc, caseElement, EXAMINER_ELEMENT_NAME, examiner); createChildElement(doc, caseElement, CASE_TYPE_ELEMENT_NAME, caseType.toString()); - createChildElement(doc, caseElement, CASE_DATABASE_ABSOLUTE_PATH_ELEMENT_NAME, caseDatabasePath); - createChildElement(doc, caseElement, CASE_DATABASE_NAME_RELATIVE_ELEMENT_NAME, caseDatabaseName); + createChildElement(doc, caseElement, CASE_DB_ABSOLUTE_PATH_ELEMENT_NAME, caseDatabasePath); + createChildElement(doc, caseElement, CASE_DB_NAME_RELATIVE_ELEMENT_NAME, caseDatabaseName); createChildElement(doc, caseElement, TEXT_INDEX_ELEMENT, textIndexName); } @@ -451,15 +463,19 @@ public final class CaseMetadata { if (null == this.caseType) { throw new CaseMetadataException("Case metadata file corrupted"); } - if (schemaVersion.equals(SCHEMA_VERSION_ONE)) { - this.caseDatabaseName = getElementTextContent(caseElement, CASE_DATABASE_NAME_ELEMENT_NAME, true); - this.textIndexName = getElementTextContent(caseElement, TEXT_INDEX_NAME_ELEMENT, true); - } else if (schemaVersion.equals(SCHEMA_VERSION_TWO)) { - this.caseDatabaseName = getElementTextContent(caseElement, CASE_DATABASE_ABSOLUTE_PATH_ELEMENT_NAME, true); - this.textIndexName = getElementTextContent(caseElement, TEXT_INDEX_ELEMENT, false); - } else { - this.caseDatabaseName = getElementTextContent(caseElement, CASE_DATABASE_NAME_RELATIVE_ELEMENT_NAME, true); - this.textIndexName = getElementTextContent(caseElement, TEXT_INDEX_ELEMENT, false); + switch (schemaVersion) { + case SCHEMA_VERSION_ONE: + this.caseDatabaseName = getElementTextContent(caseElement, CASE_DATABASE_NAME_ELEMENT_NAME, true); + this.textIndexName = getElementTextContent(caseElement, TEXT_INDEX_NAME_ELEMENT, true); + break; + case SCHEMA_VERSION_TWO: + this.caseDatabaseName = getElementTextContent(caseElement, CASE_DB_ABSOLUTE_PATH_ELEMENT_NAME, true); + this.textIndexName = getElementTextContent(caseElement, TEXT_INDEX_ELEMENT, false); + break; + default: + this.caseDatabaseName = getElementTextContent(caseElement, CASE_DB_NAME_RELATIVE_ELEMENT_NAME, true); + this.textIndexName = getElementTextContent(caseElement, TEXT_INDEX_ELEMENT, false); + break; } /* @@ -489,7 +505,7 @@ public final class CaseMetadata { * @param elementName The element name. * @param contentIsRequired Whether or not the content is required. * - * @return The text content, may be empty if not required. + * @return The text content, may be empty If not required. * * @throws CaseMetadataException If the element is missing or content is * required and it is empty. @@ -530,7 +546,7 @@ public final class CaseMetadata { * @return The full path to the case database file for a single-user case. * * @throws UnsupportedOperationException If called for a multi-user case. - * @deprecated + * @deprecated Do not use. */ @Deprecated public String getCaseDatabasePath() throws UnsupportedOperationException { diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java index 26a7fb9c26..983cbd9149 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java @@ -125,13 +125,15 @@ public final class CaseOpenAction extends CallableSystemAction implements Action JOptionPane.ERROR_MESSAGE); StartupWindowProvider.getInstance().open(); } catch (ExecutionException ex) { - logger.log(Level.SEVERE, String.format("Error opening case with metadata file path %s", path), ex); //NON-NLS - WindowManager.getDefault().getMainWindow().setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); - JOptionPane.showMessageDialog( - WindowManager.getDefault().getMainWindow(), - ex.getCause().getMessage(), //get the message of the wrapped exception - NbBundle.getMessage(this.getClass(), "CaseOpenAction.msgDlg.cantOpenCase.title"), //NON-NLS - JOptionPane.ERROR_MESSAGE); + if (null != ex.getCause() && !(ex.getCause() instanceof CaseActionCancelledException)) { + logger.log(Level.SEVERE, String.format("Error opening case with metadata file path %s", path), ex); //NON-NLS + WindowManager.getDefault().getMainWindow().setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + JOptionPane.showMessageDialog( + WindowManager.getDefault().getMainWindow(), + ex.getCause().getMessage(), //get the message of the wrapped exception + NbBundle.getMessage(this.getClass(), "CaseOpenAction.msgDlg.cantOpenCase.title"), //NON-NLS + JOptionPane.ERROR_MESSAGE); + } StartupWindowProvider.getInstance().open(); } WindowManager.getDefault().getMainWindow().setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CasePropertiesAction.java b/Core/src/org/sleuthkit/autopsy/casemodule/CasePropertiesAction.java index 3f80544101..1c0e021999 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CasePropertiesAction.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CasePropertiesAction.java @@ -22,7 +22,6 @@ import java.awt.Dimension; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; import javax.swing.Action; import javax.swing.JDialog; import javax.swing.SwingUtilities; @@ -43,11 +42,11 @@ final class CasePropertiesAction extends CallableSystemAction { CasePropertiesAction() { putValue(Action.NAME, NbBundle.getMessage(CasePropertiesAction.class, "CTL_CasePropertiesAction")); this.setEnabled(false); - Case.addEventSubscriber(Case.Events.CURRENT_CASE.toString(), new PropertyChangeListener() { - @Override - public void propertyChange(PropertyChangeEvent evt) { - setEnabled(null != evt.getNewValue()); + Case.addEventSubscriber(Case.Events.CURRENT_CASE.toString(), (PropertyChangeEvent evt) -> { + if (null == evt.getNewValue()) { + casePropertiesDialog = null; } + setEnabled(null != evt.getNewValue()); }); } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CasePropertiesPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/CasePropertiesPanel.java index 753033fe51..b6b11349ab 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CasePropertiesPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CasePropertiesPanel.java @@ -16,63 +16,57 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.sleuthkit.autopsy.casemodule; import java.nio.file.Paths; -import java.util.Map; import java.util.logging.Level; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import org.openide.DialogDescriptor; -import org.openide.DialogDisplayer; -import org.openide.NotifyDescriptor; import org.openide.util.NbBundle; import org.openide.util.actions.CallableSystemAction; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; /** - * A panel that allows the user to view various properties of the current case - * and change the display name of the case. + * A panel that allows the user to view various properties of a case and change + * the display name of the case. */ class CasePropertiesPanel extends javax.swing.JPanel { private static final long serialVersionUID = 1L; - private Case current = null; - private static JPanel caller; // panel for error + private static final Logger LOGGER = Logger.getLogger(CasePropertiesPanel.class.getName()); + private final Case theCase; - CasePropertiesPanel(Case currentCase, String crDate, String caseDir, Map imgPaths) throws CaseMetadata.CaseMetadataException { + /** + * Constructs a panel that allows the user to view various properties of the + * current case and change the display name of the case. + * + * @param aCase A case. + */ + CasePropertiesPanel(Case aCase) { initComponents(); - caseNameTextField.setText(currentCase.getDisplayName()); - String caseNumber = currentCase.getNumber(); + theCase = aCase; + caseNameTextField.setText(theCase.getDisplayName()); + String caseNumber = theCase.getNumber(); if (!caseNumber.isEmpty()) { caseNumberField.setText(caseNumber); } else { caseNumberField.setText("N/A"); } - String examiner = currentCase.getExaminer(); + String examiner = theCase.getExaminer(); if (!examiner.isEmpty()) { examinerField.setText(examiner); } else { examinerField.setText("N/A"); } - crDateField.setText(crDate); - caseDirField.setText(caseDir); - current = currentCase; - - CaseMetadata caseMetadata = currentCase.getCaseMetadata(); - if (caseMetadata.getCaseType() == Case.CaseType.SINGLE_USER_CASE) { - dbNameField.setText(Paths.get(caseMetadata.getCaseDirectory(), caseMetadata.getCaseDatabaseName()).toString()); + crDateField.setText(theCase.getCreatedDate()); + caseDirField.setText(theCase.getCaseDirectory()); + if (Case.CaseType.SINGLE_USER_CASE == theCase.getCaseType()) { + dbNameField.setText(Paths.get(theCase.getCaseDirectory(), theCase.getMetadata().getCaseDatabaseName()).toString()); } else { - dbNameField.setText(caseMetadata.getCaseDatabaseName()); - } - Case.CaseType caseType = caseMetadata.getCaseType(); - caseTypeField.setText(caseType.getLocalizedDisplayName()); - if (caseType == Case.CaseType.SINGLE_USER_CASE) { - deleteCaseButton.setEnabled(true); - } else { - deleteCaseButton.setEnabled(false); + dbNameField.setText(theCase.getMetadata().getCaseDatabaseName()); } + Case.CaseType caseType = theCase.getCaseType(); + caseTypeField.setText(caseType.getLocalizedDisplayName()); + deleteCaseButton.setEnabled(Case.CaseType.SINGLE_USER_CASE == caseType); } /** @@ -259,59 +253,35 @@ class CasePropertiesPanel extends javax.swing.JPanel { }// //GEN-END:initComponents /** - * Updates the case name. + * Updates the case display name. * * @param evt The action event */ + @NbBundle.Messages({ + "CasePropertiesPanel.errorDialog.emptyCaseNameMessage=No case name entered.", + "CasePropertiesPanel.errorDialog.invalidCaseNameMessage=Case names cannot include the following symbols: \\, /, :, *, ?, \", <, >, |" + }) private void updateCaseNameButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_updateCaseNameButtonActionPerformed - String oldCaseName = Case.getCurrentCase().getDisplayName(); - String newCaseName = caseNameTextField.getText(); - // check if the old and new case name is not equal - if (!oldCaseName.equals(newCaseName)) { + String newCaseDisplayName = caseNameTextField.getText(); + if (newCaseDisplayName.equals(theCase.getDisplayName())) { + return; + } - // check if the case name is empty - if (newCaseName.trim().isEmpty()) { - JOptionPane.showMessageDialog(caller, - NbBundle.getMessage(this.getClass(), - "CasePropertiesForm.updateCaseName.msgDlg.empty.msg"), - NbBundle.getMessage(this.getClass(), - "CasePropertiesForm.updateCaseName.msgDlg.empty.title"), - JOptionPane.ERROR_MESSAGE); - } else // check if case Name contain one of this following symbol: - // \ / : * ? " < > | - { - if (newCaseName.contains("\\") || newCaseName.contains("/") || newCaseName.contains(":") - || newCaseName.contains("*") || newCaseName.contains("?") || newCaseName.contains("\"") - || newCaseName.contains("<") || newCaseName.contains(">") || newCaseName.contains("|")) { - String errorMsg = NbBundle - .getMessage(this.getClass(), "CasePropertiesForm.updateCaseName.msgDlg.invalidSymbols.msg"); - JOptionPane.showMessageDialog(caller, errorMsg, - NbBundle.getMessage(this.getClass(), - "CasePropertiesForm.updateCaseName.msgDlg.invalidSymbols.title"), - JOptionPane.ERROR_MESSAGE); - } else { - // ask for the confirmation first - String confMsg = NbBundle - .getMessage(this.getClass(), "CasePropertiesForm.updateCaseName.confMsg.msg", oldCaseName, - newCaseName); - NotifyDescriptor d = new NotifyDescriptor.Confirmation(confMsg, - NbBundle.getMessage(this.getClass(), - "CasePropertiesForm.updateCaseName.confMsg.title"), - NotifyDescriptor.YES_NO_OPTION, NotifyDescriptor.WARNING_MESSAGE); - d.setValue(NotifyDescriptor.NO_OPTION); + if (newCaseDisplayName.trim().isEmpty()) { + MessageNotifyUtil.Message.error(Bundle.CasePropertiesPanel_errorDialog_emptyCaseNameMessage()); + return; + } - Object res = DialogDisplayer.getDefault().notify(d); - if (res != null && res == DialogDescriptor.YES_OPTION) { - // if user select "Yes" - String oldPath = current.getCaseMetadata().getFilePath().toString(); - try { - current.updateCaseName(oldCaseName, oldPath, newCaseName, oldPath); - } catch (CaseActionException ex) { - Logger.getLogger(CasePropertiesPanel.class.getName()).log(Level.WARNING, "Error: problem updating case name.", ex); //NON-NLS - } - } - } - } + if (!Case.isValidName(newCaseDisplayName)) { + MessageNotifyUtil.Message.error(Bundle.CasePropertiesPanel_errorDialog_invalidCaseNameMessage()); + return; + } + + try { + theCase.updateDisplayName(newCaseDisplayName); + } catch (CaseActionException ex) { + MessageNotifyUtil.Message.error(ex.getLocalizedMessage()); + LOGGER.log(Level.SEVERE, "Failed to update case display name", ex); //NON-NLS } }//GEN-LAST:event_updateCaseNameButtonActionPerformed diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardAction.java b/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardAction.java index 128ea4a8c3..3679587491 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardAction.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardAction.java @@ -94,15 +94,17 @@ final class NewCaseWizardAction extends CallableSystemAction { AddImageAction addImageAction = SystemAction.get(AddImageAction.class); addImageAction.actionPerformed(null); } catch (InterruptedException | ExecutionException ex) { - logger.log(Level.SEVERE, String.format("Error creating case %s", wizardDescriptor.getProperty("caseName")), ex); //NON-NLS - JOptionPane.showMessageDialog( - WindowManager.getDefault().getMainWindow(), - (ex instanceof ExecutionException ? ex.getCause().getMessage() : ex.getMessage()), - NbBundle.getMessage(this.getClass(), "CaseCreateAction.msgDlg.cantCreateCase.msg"), //NON-NLS - JOptionPane.ERROR_MESSAGE); + if (null != ex.getCause() && !(ex.getCause() instanceof CaseActionCancelledException)) { + logger.log(Level.SEVERE, String.format("Error creating case %s", wizardDescriptor.getProperty("caseName")), ex); //NON-NLS + JOptionPane.showMessageDialog( + WindowManager.getDefault().getMainWindow(), + (ex instanceof ExecutionException ? ex.getCause().getMessage() : ex.getMessage()), + NbBundle.getMessage(this.getClass(), "CaseCreateAction.msgDlg.cantCreateCase.msg"), //NON-NLS + JOptionPane.ERROR_MESSAGE); + } + doFailedCaseCleanup(wizardDescriptor); StartupWindowProvider.getInstance().close(); StartupWindowProvider.getInstance().open(); - doFailedCaseCleanup(wizardDescriptor); } finally { WindowManager.getDefault().getMainWindow().setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardPanel1.java b/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardPanel1.java index dcfaab449c..1660381152 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardPanel1.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardPanel1.java @@ -38,29 +38,21 @@ import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ModuleSettings; /** - * The "New Case" wizard panel with a component on it. This class represents - * data of wizard step. It defers creation and initialization of UI component of - * wizard panel into getComponent() method. + * The first panel of the New Case wizard. */ class NewCaseWizardPanel1 implements WizardDescriptor.ValidatingPanel { - /** - * The visual component that displays this panel. If you need to access the - * component from this class, just use getComponent(). - */ - private NewCaseVisualPanel1 component; - private Boolean isFinish = false; - private static String createdDirectory; - private static final String PROP_BASECASE = "LBL_BaseCase_PATH"; //NON-NLS private static final Logger logger = Logger.getLogger(NewCaseWizardPanel1.class.getName()); + private static final String PROP_BASECASE = "LBL_BaseCase_PATH"; //NON-NLS + private static String createdDirectory; + private final Set listeners = new HashSet<>(1); + private NewCaseVisualPanel1 component; + private boolean isFinish; /** - * Get the visual component for the panel. In this template, the component - * is kept separate. This can be more efficient: if the wizard is created - * but never displayed, or not all panels are displayed, it is better to - * create only those which really need to be visible. + * Get the visual component for the panel. * - * @return component the UI component of this wizard panel + * @return The UI component of this wizard panel */ @Override public NewCaseVisualPanel1 getComponent() { @@ -71,65 +63,57 @@ class NewCaseWizardPanel1 implements WizardDescriptor.ValidatingPanel listeners = new HashSet<>(1); // or can use ChangeSupport in NB 6.0 /** - * Adds a listener to changes of the panel's validity. + * Adds a change listener to this panel. * - * @param l the change listener to add + * @param listener The change listener to add. */ @Override - public final void addChangeListener(ChangeListener l) { + public final void addChangeListener(ChangeListener listener) { synchronized (listeners) { - listeners.add(l); + listeners.add(listener); } } /** - * Removes a listener to changes of the panel's validity. + * Removes a change listener from this panel. * - * @param l the change listener to move + * @param listener The change listener to remove. */ @Override - public final void removeChangeListener(ChangeListener l) { + public final void removeChangeListener(ChangeListener listener) { synchronized (listeners) { - listeners.remove(l); + listeners.remove(listener); } } /** - * This method is auto-generated. It seems that this method is used to - * listen to any change in this wizard panel. + * Notifies any registerd change listeners of a change in the panel. */ protected final void fireChangeEvent() { Iterator it; @@ -153,12 +137,8 @@ class NewCaseWizardPanel1 implements WizardDescriptor.ValidatingPanel sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,35 +28,19 @@ import org.openide.WizardValidationException; import org.openide.util.HelpCtx; import org.openide.windows.WindowManager; import java.awt.Cursor; -import org.sleuthkit.autopsy.casemodule.Case.CaseType; /** - * The "New Case" wizard panel with a component on it. This class represents - * data of wizard step. It defers creation and initialization of UI component of - * wizard panel into getComponent() method. - * - * @author jantonius + * The second panel of the New Case wizard. */ class NewCaseWizardPanel2 implements WizardDescriptor.ValidatingPanel { - /** - * The visual component that displays this panel. If you need to access the - * component from this class, just use getComponent(). - */ private NewCaseVisualPanel2 component; - private Boolean isFinish = true; - private String caseName; - private String caseDir; - private String createdDirectory; - private CaseType caseType; + private final Set listeners = new HashSet<>(1); /** - * Get the visual component for the panel. In this template, the component - * is kept separate. This can be more efficient: if the wizard is created - * but never displayed, or not all panels are displayed, it is better to - * create only those which really need to be visible. + * Get the visual component for the panel. * - * @return component the UI component of this wizard panel + * @return component The UI component of this wizard panel. */ @Override public NewCaseVisualPanel2 getComponent() { @@ -67,17 +51,17 @@ class NewCaseWizardPanel2 implements WizardDescriptor.ValidatingPanel listeners = new HashSet(1); // or can use ChangeSupport in NB 6.0 /** - * Adds a listener to changes of the panel's validity. + * Adds a change listener to this panel. * - * @param l the change listener to add + * @param listener The change listener to add. */ @Override - public final void addChangeListener(ChangeListener l) { + public final void addChangeListener(ChangeListener listener) { synchronized (listeners) { - listeners.add(l); + listeners.add(listener); } } /** - * Removes a listener to changes of the panel's validity. + * Removes a change listener from this panel. * - * @param l the change listener to move + * @param listener The change listener to remove. */ @Override - public final void removeChangeListener(ChangeListener l) { + public final void removeChangeListener(ChangeListener listener) { synchronized (listeners) { - listeners.remove(l); + listeners.remove(listener); } } /** - * This method is auto-generated. It seems that this method is used to - * listen to any change in this wizard panel. + * Notifies any registerd change listeners of a change in the panel. */ protected final void fireChangeEvent() { Iterator it; @@ -138,10 +114,6 @@ class NewCaseWizardPanel2 implements WizardDescriptor.ValidatingPanel { - JOptionPane.showMessageDialog( - WindowManager.getDefault().getMainWindow(), - ex.getMessage(), // Should be user-friendly - NbBundle.getMessage(this.getClass(), "CaseOpenAction.msgDlg.cantOpenCase.title"), //NON-NLS - JOptionPane.ERROR_MESSAGE); + if (null != ex.getCause() && !(ex.getCause() instanceof CaseActionCancelledException)) { + logger.log(Level.SEVERE, String.format("Error opening case with metadata file path %s", casePath), ex); //NON-NLS + JOptionPane.showMessageDialog( + WindowManager.getDefault().getMainWindow(), + ex.getMessage(), // Should be user-friendly + NbBundle.getMessage(this.getClass(), "CaseOpenAction.msgDlg.cantOpenCase.title"), //NON-NLS + JOptionPane.ERROR_MESSAGE); + } StartupWindowProvider.getInstance().open(); }); } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/RecentCases.java b/Core/src/org/sleuthkit/autopsy/casemodule/RecentCases.java index 9ea9214c36..49e480450f 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/RecentCases.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/RecentCases.java @@ -407,9 +407,11 @@ final class RecentCases extends CallableSystemAction implements Presenter.Menu { String[] casePaths = new String[LENGTH]; String currentCasePath = null; try { - currentCasePath = Case.getCurrentCase().getCaseMetadata().getFilePath().toString(); + currentCasePath = Case.getCurrentCase().getMetadata().getFilePath().toString(); } catch (IllegalStateException ex) { - // in case there is no current case. + /* + * There may be no current case. + */ } Iterator mostRecentFirst = recentCases.descendingIterator(); diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/RecentItems.java b/Core/src/org/sleuthkit/autopsy/casemodule/RecentItems.java index d46fce31cb..b60cb3e800 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/RecentItems.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/RecentItems.java @@ -65,17 +65,20 @@ class RecentItems implements ActionListener { try { Case.openAsCurrentCase(caseMetaDataFilePath); } catch (CaseActionException ex) { - logger.log(Level.SEVERE, String.format("Error opening case with metadata file path %s", caseMetaDataFilePath), ex); //NON-NLS SwingUtilities.invokeLater(() -> { - JOptionPane.showMessageDialog( - WindowManager.getDefault().getMainWindow(), - ex.getMessage(), - NbBundle.getMessage(RecentItems.this.getClass(), "CaseOpenAction.msgDlg.cantOpenCase.title"), //NON-NLS - JOptionPane.ERROR_MESSAGE); + if (null != ex.getCause() && !(ex.getCause() instanceof CaseActionCancelledException)) { + logger.log(Level.SEVERE, String.format("Error opening case with metadata file path %s", caseMetaDataFilePath), ex); //NON-NLS + JOptionPane.showMessageDialog( + WindowManager.getDefault().getMainWindow(), + ex.getMessage(), + NbBundle.getMessage(RecentItems.this.getClass(), "CaseOpenAction.msgDlg.cantOpenCase.title"), //NON-NLS + JOptionPane.ERROR_MESSAGE); + } StartupWindowProvider.getInstance().open(); }); } - }).start(); + }). + start(); } } } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/SingleUserCaseConverter.java b/Core/src/org/sleuthkit/autopsy/casemodule/SingleUserCaseConverter.java index 50494f47a0..446c03b6c8 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/SingleUserCaseConverter.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/SingleUserCaseConverter.java @@ -204,8 +204,8 @@ public class SingleUserCaseConverter { icd.getNewCaseName(), icd.getNewCaseName(), oldCaseMetadata.getCaseNumber(), - oldCaseMetadata.getExaminer(), - dbName); + oldCaseMetadata.getExaminer()); + newCaseMetadata.setCaseDatabaseName(dbName); // Set created date. This calls writefile, no need to call it again newCaseMetadata.setCreatedDate(oldCaseMetadata.getCreatedDate()); newCaseMetadata.setCreatedByVersion(oldCaseMetadata.getCreatedByVersion()); diff --git a/Core/src/org/sleuthkit/autopsy/framework/LoggingProgressIndicator.java b/Core/src/org/sleuthkit/autopsy/framework/LoggingProgressIndicator.java index 3fcad59638..83e7db95e0 100644 --- a/Core/src/org/sleuthkit/autopsy/framework/LoggingProgressIndicator.java +++ b/Core/src/org/sleuthkit/autopsy/framework/LoggingProgressIndicator.java @@ -59,7 +59,7 @@ public final class LoggingProgressIndicator implements ProgressIndicator { @Override public void progress(int workUnitsCompleted) { - LOGGER.log(Level.INFO, "{1} of {2} total work units completed", new Object[]{workUnitsCompleted, this.totalWorkUnits}); + LOGGER.log(Level.INFO, "{0} of {1} total work units completed", new Object[]{workUnitsCompleted, this.totalWorkUnits}); } @Override @@ -68,8 +68,8 @@ public final class LoggingProgressIndicator implements ProgressIndicator { } @Override - public void finish(String message) { - LOGGER.log(Level.INFO, "{0} finished", message); + public void finish() { + LOGGER.log(Level.INFO, "Finished"); } } diff --git a/Core/src/org/sleuthkit/autopsy/framework/ModalDialogProgressIndicator.java b/Core/src/org/sleuthkit/autopsy/framework/ModalDialogProgressIndicator.java index c5df4c8cfa..2527b72603 100644 --- a/Core/src/org/sleuthkit/autopsy/framework/ModalDialogProgressIndicator.java +++ b/Core/src/org/sleuthkit/autopsy/framework/ModalDialogProgressIndicator.java @@ -21,6 +21,8 @@ package org.sleuthkit.autopsy.framework; import java.awt.Dialog; import java.awt.Frame; import java.awt.event.ActionListener; +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; import javax.swing.JDialog; import javax.swing.SwingUtilities; import org.openide.DialogDescriptor; @@ -30,14 +32,17 @@ import org.openide.util.HelpCtx; /** * A progress indicator that displays progress using a modal dialog with a * message label, a progress bar, and optionally, a configurable set of buttons - * with a button listener. + * with a button listener. Setting a cancelling flag which locks in a cancelling + * message and an indeterminate progress bar is supported. */ +@ThreadSafe public final class ModalDialogProgressIndicator implements ProgressIndicator { private final Frame parent; private final ProgressPanel progressPanel; private final Dialog dialog; - private final ActionListener buttonListener; + @GuardedBy("this") + private boolean cancelling; /** * Creates a progress indicator that displays progress using a modal dialog @@ -54,6 +59,7 @@ public final class ModalDialogProgressIndicator implements ProgressIndicator { public ModalDialogProgressIndicator(Frame parent, String title, Object[] buttonLabels, Object focusedButtonLabel, ActionListener buttonListener) { this.parent = parent; progressPanel = new ProgressPanel(); + progressPanel.setIndeterminate(true); DialogDescriptor dialogDescriptor = new DialogDescriptor( progressPanel, title, @@ -64,7 +70,6 @@ public final class ModalDialogProgressIndicator implements ProgressIndicator { HelpCtx.DEFAULT_HELP, buttonListener); dialog = DialogDisplayer.getDefault().createDialog(dialogDescriptor); - this.buttonListener = buttonListener; } /** @@ -77,31 +82,10 @@ public final class ModalDialogProgressIndicator implements ProgressIndicator { public ModalDialogProgressIndicator(Frame parent, String title) { this.parent = parent; progressPanel = new ProgressPanel(); + progressPanel.setIndeterminate(true); dialog = new JDialog(parent, title, true); dialog.add(progressPanel); dialog.pack(); - buttonListener = null; - } - - /** - * Calls setVisible on the underlying modal dialog. - * - * @param isVisible True or false. - */ - public void setVisible(boolean isVisible) { - if (isVisible) { - dialog.setLocationRelativeTo(parent); - } - this.dialog.setVisible(isVisible); - } - - /** - * Gets the button listener for the dialog, if there is one. - * - * @return The button listener or null. - */ - public ActionListener getButtonListener() { - return buttonListener; } /** @@ -112,14 +96,14 @@ public final class ModalDialogProgressIndicator implements ProgressIndicator { * @param totalWorkUnits The total number of work units. */ @Override - public void start(String message, int totalWorkUnits) { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - progressPanel.setInderminate(false); - progressPanel.setMessage(message); - progressPanel.setMaximum(totalWorkUnits); - } + public synchronized void start(String message, int totalWorkUnits) { + cancelling = false; + SwingUtilities.invokeLater(() -> { + progressPanel.setIndeterminate(false); + progressPanel.setMessage(message); + progressPanel.setMaximum(totalWorkUnits); + dialog.setLocationRelativeTo(parent); + this.dialog.setVisible(true); }); } @@ -130,13 +114,28 @@ public final class ModalDialogProgressIndicator implements ProgressIndicator { * @param message The initial progress message. */ @Override - public void start(String message) { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - progressPanel.setInderminate(true); - progressPanel.setMessage(message); - } + public synchronized void start(String message) { + cancelling = false; + SwingUtilities.invokeLater(() -> { + progressPanel.setIndeterminate(true); + progressPanel.setMessage(message); + dialog.setLocationRelativeTo(parent); + this.dialog.setVisible(true); + }); + } + + /** + * Sets a cancelling message and makes the progress bar indeterminate. Once + * cancel has been called, the progress indicator no longer accepts updates + * unless start is called again. + * + * @param cancellingMessage + */ + public synchronized void setCancelling(String cancellingMessage) { + cancelling = true; + SwingUtilities.invokeLater(() -> { + progressPanel.setIndeterminate(false); + progressPanel.setMessage(cancellingMessage); }); } @@ -147,14 +146,13 @@ public final class ModalDialogProgressIndicator implements ProgressIndicator { * @param message The initial progress message. */ @Override - public void switchToIndeterminate(String message) { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - progressPanel.setInderminate(true); + public synchronized void switchToIndeterminate(String message) { + if (!cancelling) { + SwingUtilities.invokeLater(() -> { + progressPanel.setIndeterminate(true); progressPanel.setMessage(message); - } - }); + }); + } } /** @@ -166,16 +164,15 @@ public final class ModalDialogProgressIndicator implements ProgressIndicator { * @param totalWorkUnits The total number of work units to be completed. */ @Override - public void switchToDeterminate(String message, int workUnitsCompleted, int totalWorkUnits) { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - progressPanel.setInderminate(false); + public synchronized void switchToDeterminate(String message, int workUnitsCompleted, int totalWorkUnits) { + if (!cancelling) { + SwingUtilities.invokeLater(() -> { + progressPanel.setIndeterminate(false); progressPanel.setMessage(message); progressPanel.setMaximum(totalWorkUnits); progressPanel.setCurrent(workUnitsCompleted); - } - }); + }); + } } /** @@ -184,13 +181,12 @@ public final class ModalDialogProgressIndicator implements ProgressIndicator { * @param message The progress message. */ @Override - public void progress(String message) { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { + public synchronized void progress(String message) { + if (!cancelling) { + SwingUtilities.invokeLater(() -> { progressPanel.setMessage(message); - } - }); + }); + } } /** @@ -201,13 +197,12 @@ public final class ModalDialogProgressIndicator implements ProgressIndicator { * @param workUnitsCompleted Number of work units completed so far. */ @Override - public void progress(int workUnitsCompleted) { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { + public synchronized void progress(int workUnitsCompleted) { + if (!cancelling) { + SwingUtilities.invokeLater(() -> { progressPanel.setCurrent(workUnitsCompleted); - } - }); + }); + } } /** @@ -219,28 +214,22 @@ public final class ModalDialogProgressIndicator implements ProgressIndicator { * @param workUnitsCompleted Number of work units completed so far. */ @Override - public void progress(String message, int workUnitsCompleted) { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { + public synchronized void progress(String message, int workUnitsCompleted) { + if (!cancelling) { + SwingUtilities.invokeLater(() -> { progressPanel.setMessage(message); progressPanel.setCurrent(workUnitsCompleted); - } - }); + }); + } } /** * Finishes the progress indicator when the task is completed. - * - * @param message The finished message. */ @Override - public void finish(String message) { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - progressPanel.setMessage(message); - } + public synchronized void finish() { + SwingUtilities.invokeLater(() -> { + this.dialog.setVisible(false); }); } diff --git a/Core/src/org/sleuthkit/autopsy/framework/ProgressIndicator.java b/Core/src/org/sleuthkit/autopsy/framework/ProgressIndicator.java index 0f51d98d75..6f91dd3231 100644 --- a/Core/src/org/sleuthkit/autopsy/framework/ProgressIndicator.java +++ b/Core/src/org/sleuthkit/autopsy/framework/ProgressIndicator.java @@ -46,6 +46,7 @@ public interface ProgressIndicator { /** * Switches the progress indicator to indeterminate mode (the total number * of work units to be completed is unknown). + * * @param message The initial progress message. */ public void switchToIndeterminate(String message); @@ -54,7 +55,7 @@ public interface ProgressIndicator { * Switches the progress indicator to determinate mode (the total number of * work units to be completed is known). * - * @param message The initial progress message. + * @param message The initial progress message. * @param workUnitsCompleted The number of work units completed so far. * @param totalWorkUnits The total number of work units to be completed. */ @@ -88,9 +89,7 @@ public interface ProgressIndicator { /** * Finishes the progress indicator when the task is completed. - * - * @param message The finished message. */ - void finish(String message); + void finish(); } diff --git a/Core/src/org/sleuthkit/autopsy/framework/ProgressPanel.java b/Core/src/org/sleuthkit/autopsy/framework/ProgressPanel.java index 4957a317b6..4130eb4852 100644 --- a/Core/src/org/sleuthkit/autopsy/framework/ProgressPanel.java +++ b/Core/src/org/sleuthkit/autopsy/framework/ProgressPanel.java @@ -35,7 +35,7 @@ class ProgressPanel extends javax.swing.JPanel { this.progressMessage.setText(message); } - void setInderminate(boolean indeterminate) { + void setIndeterminate(boolean indeterminate) { this.progressBar.setIndeterminate(indeterminate); } diff --git a/Core/src/org/sleuthkit/autopsy/framework/SilentProgressIndicator.java b/Core/src/org/sleuthkit/autopsy/framework/SilentProgressIndicator.java index 8152a097c9..b7b03b8c44 100644 --- a/Core/src/org/sleuthkit/autopsy/framework/SilentProgressIndicator.java +++ b/Core/src/org/sleuthkit/autopsy/framework/SilentProgressIndicator.java @@ -54,7 +54,7 @@ public class SilentProgressIndicator implements ProgressIndicator { } @Override - public void finish(String message) { + public void finish() { } } diff --git a/Core/src/org/sleuthkit/autopsy/imagewriter/ImageWriterService.java b/Core/src/org/sleuthkit/autopsy/imagewriter/ImageWriterService.java index 594532cbc6..cf6f042292 100644 --- a/Core/src/org/sleuthkit/autopsy/imagewriter/ImageWriterService.java +++ b/Core/src/org/sleuthkit/autopsy/imagewriter/ImageWriterService.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2016 Basis Technology Corp. + * Copyright 2011-2017 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,98 +25,99 @@ import org.openide.DialogDisplayer; import org.openide.NotifyDescriptor; import org.openide.util.NbBundle; import org.openide.util.lookup.ServiceProvider; -import org.openide.util.lookup.ServiceProviders; -import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.framework.AutopsyService; -@ServiceProviders(value = {@ServiceProvider(service = AutopsyService.class)}) - /** - * Creates and handles closing of ImageWriter objects. - * Currently, ImageWriter is only enabled for local disks, and local disks can - * not be processed in multi user mode. If ImageWriter is ever enabled for multi user - * cases this code will need to be revised. + * Creates and handles closing of ImageWriter objects. Currently, ImageWriter is + * only enabled for local disks, and local disks can not be processed in multi + * user mode. If ImageWriter is ever enabled for multi user cases this code will + * need to be revised. */ - +@ServiceProvider(service = AutopsyService.class) public class ImageWriterService implements AutopsyService { private static final List imageWriters = new ArrayList<>(); // Contains all Image Writer objects private static final Object imageWritersLock = new Object(); // Get this lock before accessing currentImageWriters - + /** * Create an image writer object for the given data source ID. + * * @param imageId ID for the image */ - public static void createImageWriter(Long imageId, ImageWriterSettings settings){ - + public static void createImageWriter(Long imageId, ImageWriterSettings settings) { + // ImageWriter objects are created during the addImageTask. They can not arrive while // we're closing case resources so we don't need to worry about one showing up while // doing our close/cleanup. - synchronized(imageWritersLock){ + synchronized (imageWritersLock) { ImageWriter writer = new ImageWriter(imageId, settings); writer.subscribeToEvents(); imageWriters.add(writer); } } - + @Override public String getServiceName() { return NbBundle.getMessage(this.getClass(), "ImageWriterService.serviceName"); } - + @Override public void closeCaseResources(CaseContext context) throws AutopsyServiceException { - context.getProgressIndicator().progress(NbBundle.getMessage(this.getClass(), "ImageWriterService.waitingForVHDs")); - - synchronized(imageWritersLock){ + synchronized (imageWritersLock) { + if (imageWriters.isEmpty()) { + return; + } + + context.getProgressIndicator().progress(NbBundle.getMessage(this.getClass(), "ImageWriterService.waitingForVHDs")); + // If any of our ImageWriter objects haven't started the finish task, set the cancel flag // to make sure they don't start now. The reason they haven't started is that // ingest was not complete, and the user already confirmed that they want to exit // even though ingest is not complete so we will take that to mean that they // also don't want to wait for Image Writer. - for(ImageWriter writer: imageWriters){ + for (ImageWriter writer : imageWriters) { writer.cancelIfNotStarted(); } - + // Test whether any finishImage tasks are in progress boolean jobsAreInProgress = false; - for(ImageWriter writer: imageWriters){ - if(writer.jobIsInProgress()){ + for (ImageWriter writer : imageWriters) { + if (writer.jobIsInProgress()) { jobsAreInProgress = true; break; } } - - if(jobsAreInProgress){ + + if (jobsAreInProgress) { // If jobs are in progress, ask the user if they want to wait for them to complete NotifyDescriptor descriptor = new NotifyDescriptor.Confirmation( - NbBundle.getMessage(this.getClass(), "ImageWriterService.shouldWait"), - NbBundle.getMessage(this.getClass(), "ImageWriterService.localDisk"), - NotifyDescriptor.YES_NO_OPTION, - NotifyDescriptor.WARNING_MESSAGE); + NbBundle.getMessage(this.getClass(), "ImageWriterService.shouldWait"), + NbBundle.getMessage(this.getClass(), "ImageWriterService.localDisk"), + NotifyDescriptor.YES_NO_OPTION, + NotifyDescriptor.WARNING_MESSAGE); descriptor.setValue(NotifyDescriptor.NO_OPTION); Object response = DialogDisplayer.getDefault().notify(descriptor); - - if(response == DialogDescriptor.NO_OPTION){ + + if (response == DialogDescriptor.NO_OPTION) { // Cancel all the jobs - for(ImageWriter writer: imageWriters){ + for (ImageWriter writer : imageWriters) { writer.cancelJob(); } } - + // Wait for all finishImage jobs to complete. If the jobs got cancelled // this will be very fast. - for(ImageWriter writer: imageWriters){ + for (ImageWriter writer : imageWriters) { writer.waitForJobToFinish(); } - + } - + // Stop listening for events - for(ImageWriter writer: imageWriters){ + for (ImageWriter writer : imageWriters) { writer.unsubscribeFromEvents(); } - + // Clear out the list of Image Writers imageWriters.clear(); } diff --git a/Core/src/org/sleuthkit/autopsy/test/TestAutopsyService.java b/Core/src/org/sleuthkit/autopsy/test/TestAutopsyService.java index b550ca647d..f3e30a6b63 100644 --- a/Core/src/org/sleuthkit/autopsy/test/TestAutopsyService.java +++ b/Core/src/org/sleuthkit/autopsy/test/TestAutopsyService.java @@ -53,7 +53,7 @@ public class TestAutopsyService implements AutopsyService { progressIndicator.progress(80); Thread.sleep(1000L); progressIndicator.progress(100); - progressIndicator.finish("First task completed by Test Autopsy Service."); + progressIndicator.finish(); progressIndicator.start("Test Autopsy Service doing second task..."); for (int i = 0; i < 10000; ++i) { logger.log(Level.INFO, "Test Autopsy Service simulating work on second task"); @@ -62,7 +62,7 @@ public class TestAutopsyService implements AutopsyService { break; } } - progressIndicator.finish("Second task completed by Test Autopsy Service."); + progressIndicator.finish(); } catch (InterruptedException ex) { logger.log(Level.INFO, "Test Autopsy Service interrupted (cancelled) while doing first task, cancel requested = {0}", context.cancelRequested()); } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/SolrSearchService.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/SolrSearchService.java index acde9d77fc..4018ce7698 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/SolrSearchService.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/SolrSearchService.java @@ -200,6 +200,10 @@ public class SolrSearchService implements KeywordSearchService, AutopsyService { "SolrSearch.openCore.msg=Opening text index", "SolrSearch.complete.msg=Text index successfully opened"}) public void openCaseResources(CaseContext context) throws AutopsyServiceException { + if (context.cancelRequested()) { + return; + } + ProgressIndicator progress = context.getProgressIndicator(); int totalNumProgressUnits = 7; int progressUnitsCompleted = 0;