diff --git a/Core/ivy.xml b/Core/ivy.xml index 55468a600f..d40831b773 100644 --- a/Core/ivy.xml +++ b/Core/ivy.xml @@ -15,7 +15,7 @@ - + diff --git a/Core/nbproject/project.properties b/Core/nbproject/project.properties index e3895cc49d..ddb1df5848 100644 --- a/Core/nbproject/project.properties +++ b/Core/nbproject/project.properties @@ -12,7 +12,7 @@ file.reference.sevenzipjbinding-AllPlatforms.jar=release/modules/ext/sevenzipjbi file.reference.sevenzipjbinding.jar=release/modules/ext/sevenzipjbinding.jar file.reference.sqlite-jdbc-3.8.11.jar=release/modules/ext/sqlite-jdbc-3.8.11.jar file.reference.StixLib.jar=release/modules/ext/StixLib.jar -file.reference.tika-core-1.5.jar=release/modules/ext/tika-core-1.5.jar +file.reference.tika-core-1.14.jar=release/modules/ext/tika-core-1.14.jar file.reference.Tsk_DataModel_PostgreSQL.jar=release/modules/ext/Tsk_DataModel_PostgreSQL.jar file.reference.xmpcore-5.1.2.jar=release/modules/ext/xmpcore-5.1.2.jar file.reference.curator-client-2.8.0.jar=release/modules/ext/curator-client-2.8.0.jar diff --git a/Core/nbproject/project.xml b/Core/nbproject/project.xml index 2d905e7e49..eb4739d209 100644 --- a/Core/nbproject/project.xml +++ b/Core/nbproject/project.xml @@ -243,6 +243,7 @@ net.sf.sevenzipjbinding.simple net.sf.sevenzipjbinding.simple.impl org.sleuthkit.autopsy.actions + org.sleuthkit.autopsy.appservices org.sleuthkit.autopsy.casemodule org.sleuthkit.autopsy.casemodule.events org.sleuthkit.autopsy.casemodule.services @@ -254,17 +255,18 @@ org.sleuthkit.autopsy.corecomponents org.sleuthkit.autopsy.coreutils org.sleuthkit.autopsy.datamodel + org.sleuthkit.autopsy.datasourceprocessors org.sleuthkit.autopsy.directorytree org.sleuthkit.autopsy.events org.sleuthkit.autopsy.externalresults org.sleuthkit.autopsy.filesearch - org.sleuthkit.autopsy.framework org.sleuthkit.autopsy.ingest org.sleuthkit.autopsy.keywordsearchservice org.sleuthkit.autopsy.menuactions org.sleuthkit.autopsy.modules.filetypeid org.sleuthkit.autopsy.modules.hashdatabase org.sleuthkit.autopsy.modules.vmextractor + org.sleuthkit.autopsy.progress org.sleuthkit.autopsy.report org.sleuthkit.datamodel @@ -325,8 +327,8 @@ release/modules/ext/sevenzipjbinding-AllPlatforms.jar - ext/tika-core-1.5.jar - release/modules/ext/tika-core-1.5.jar + ext/tika-core-1.14.jar + release/modules/ext/tika-core-1.14.jar ext/metadata-extractor-2.8.1.jar diff --git a/Core/src/org/sleuthkit/autopsy/actions/DeleteFileBlackboardArtifactTagAction.java b/Core/src/org/sleuthkit/autopsy/actions/DeleteFileBlackboardArtifactTagAction.java index afb17804b7..d9fd5d364f 100755 --- a/Core/src/org/sleuthkit/autopsy/actions/DeleteFileBlackboardArtifactTagAction.java +++ b/Core/src/org/sleuthkit/autopsy/actions/DeleteFileBlackboardArtifactTagAction.java @@ -174,10 +174,6 @@ public class DeleteFileBlackboardArtifactTagAction extends AbstractAction implem for(BlackboardArtifactTag artifactTag : existingTagsList) { if(tagDisplayName.equals(artifactTag.getName().getDisplayName())) { JMenuItem tagNameItem = new JMenuItem(tagDisplayName); - // for the bookmark tag name only, added shortcut label - if (tagDisplayName.equals(NbBundle.getMessage(AddTagAction.class, "AddBookmarkTagAction.bookmark.text"))) { - tagNameItem.setAccelerator(AddBookmarkTagAction.BOOKMARK_SHORTCUT); - } tagNameItem.addActionListener((ActionEvent e) -> { deleteTag(tagName, artifactTag, artifact.getArtifactID()); }); diff --git a/Core/src/org/sleuthkit/autopsy/actions/DeleteFileContentTagAction.java b/Core/src/org/sleuthkit/autopsy/actions/DeleteFileContentTagAction.java index 5cc37e3900..e49e0f9170 100755 --- a/Core/src/org/sleuthkit/autopsy/actions/DeleteFileContentTagAction.java +++ b/Core/src/org/sleuthkit/autopsy/actions/DeleteFileContentTagAction.java @@ -159,8 +159,6 @@ public class DeleteFileContentTagAction extends AbstractAction implements Presen // a tag with the associated tag name. if (null != tagNamesMap && !tagNamesMap.isEmpty()) { try { - /*List existingTagsList = - Case.getCurrentCase().getServices().getTagsManager().getBlackboardArtifactTagsByArtifact(artifact);*/ List existingTagsList = Case.getCurrentCase().getServices().getTagsManager() .getContentTagsByContent(file); @@ -172,10 +170,6 @@ public class DeleteFileContentTagAction extends AbstractAction implements Presen for(ContentTag contentTag : existingTagsList) { if(tagDisplayName.equals(contentTag.getName().getDisplayName())) { JMenuItem tagNameItem = new JMenuItem(tagDisplayName); - // for the bookmark tag name only, added shortcut label - if (tagDisplayName.equals(NbBundle.getMessage(AddTagAction.class, "AddBookmarkTagAction.bookmark.text"))) { - tagNameItem.setAccelerator(AddBookmarkTagAction.BOOKMARK_SHORTCUT); - } tagNameItem.addActionListener((ActionEvent e) -> { deleteTag(tagName, contentTag, file.getId()); }); diff --git a/Core/src/org/sleuthkit/autopsy/framework/AutopsyService.java b/Core/src/org/sleuthkit/autopsy/appservices/AutopsyService.java similarity index 98% rename from Core/src/org/sleuthkit/autopsy/framework/AutopsyService.java rename to Core/src/org/sleuthkit/autopsy/appservices/AutopsyService.java index 2869b550f3..6e07fbe5fe 100644 --- a/Core/src/org/sleuthkit/autopsy/framework/AutopsyService.java +++ b/Core/src/org/sleuthkit/autopsy/appservices/AutopsyService.java @@ -16,9 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.framework; +package org.sleuthkit.autopsy.appservices; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.progress.ProgressIndicator; /** * An interface for services that report status and may manage case and diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties index 3570da2497..b1c85a56b0 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..b12f0b7597 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; @@ -90,14 +92,15 @@ import org.sleuthkit.autopsy.coreutils.Version; import org.sleuthkit.autopsy.events.AutopsyEvent; import org.sleuthkit.autopsy.events.AutopsyEventException; import org.sleuthkit.autopsy.events.AutopsyEventPublisher; -import org.sleuthkit.autopsy.framework.AutopsyService; -import org.sleuthkit.autopsy.framework.AutopsyService.CaseContext; -import org.sleuthkit.autopsy.framework.LoggingProgressIndicator; -import org.sleuthkit.autopsy.framework.ModalDialogProgressIndicator; -import org.sleuthkit.autopsy.framework.ProgressIndicator; +import org.sleuthkit.autopsy.appservices.AutopsyService; +import org.sleuthkit.autopsy.appservices.AutopsyService.CaseContext; +import org.sleuthkit.autopsy.progress.LoggingProgressIndicator; +import org.sleuthkit.autopsy.progress.ModalDialogProgressIndicator; +import org.sleuthkit.autopsy.progress.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,26 @@ 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.", + "Case.exceptionMessage.cannotOpenMultiUserCaseNoSettings=Multi-user settings are missing (see Tools, Options, Multi-user tab), cannot open a multi-user case." }) 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); } + if (CaseType.MULTI_USER_CASE == metadata.getCaseType() && !UserPreferences.getIsMultiUserModeEnabled()) { + throw new CaseActionException(Bundle.Case_exceptionMessage_cannotOpenMultiUserCaseNoSettings()); + } + openAsCurrentCase(new Case(metadata), false); } /** @@ -556,45 +496,48 @@ 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(); } @@ -605,20 +548,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); } @@ -636,40 +576,32 @@ public class Case { * exception. */ @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.cannotGetLockToDeleteCase=Cannot delete case because it is open for another user or there is a problem with the coordination service." }) public static void deleteCase(CaseMetadata metadata) throws CaseActionException { - synchronized (currentCaseWriteLock) { - if (null != currentCase && 0 == metadata.getCaseDirectory().compareTo(metadata.getCaseDirectory())) { - throw new CaseActionException(Bundle.Case_deleteException_couldNotDeleteCase(Bundle.Case_exceptionMessage_cannotDeleteCurrentCase())); + synchronized (caseActionSerializationLock) { + if (null != currentCase) { + throw new CaseActionException(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 +610,80 @@ 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) { + throw new CaseActionException(Bundle.Case_exceptionMessage_cannotGetLockToDeleteCase(), 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 +821,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.progress(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.progress(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()); + progressIndicator.progress(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 +894,10 @@ public class Case { RecentCases.getInstance().removeRecentCase(metadata.getCaseDisplayName(), metadata.getFilePath().toString()); }); } + + if (errorsOccurred) { + throw new CaseActionException(Bundle.Case_exceptionMessage_errorsDeletingCase()); + } } /** @@ -938,8 +919,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 +930,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 +1109,7 @@ public class Case { * @return The case type. */ public CaseType getCaseType() { - return getCaseMetadata().getCaseType(); + return metadata.getCaseType(); } /** @@ -1146,7 +1118,7 @@ public class Case { * @return case The case create date. */ public String getCreatedDate() { - return getCaseMetadata().getCreatedDate(); + return metadata.getCreatedDate(); } /** @@ -1155,7 +1127,7 @@ public class Case { * @return The case name. */ public String getName() { - return getCaseMetadata().getCaseName(); + return metadata.getCaseName(); } /** @@ -1164,7 +1136,7 @@ public class Case { * @return The case display name. */ public String getDisplayName() { - return getCaseMetadata().getCaseDisplayName(); + return metadata.getCaseDisplayName(); } /** @@ -1173,7 +1145,7 @@ public class Case { * @return The case number */ public String getNumber() { - return caseMetadata.getCaseNumber(); + return metadata.getCaseNumber(); } /** @@ -1182,7 +1154,7 @@ public class Case { * @return The examiner name. */ public String getExaminer() { - return caseMetadata.getExaminer(); + return metadata.getExaminer(); } /** @@ -1191,7 +1163,7 @@ public class Case { * @return The top-level case directory path. */ public String getCaseDirectory() { - return caseMetadata.getCaseDirectory(); + return metadata.getCaseDirectory(); } /** @@ -1205,7 +1177,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 +1301,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 +1469,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 +1512,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 +1671,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 +1781,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 +1816,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 +1827,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 +1867,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,125 +1890,130 @@ 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()) { + throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser()); } } - /* - * No tasks left, simply shut down the executor. - */ - executor.shutdown(); } /** * Closes the 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 +2028,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 +2045,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 +2086,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 +2104,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 +2145,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 +2155,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 +2178,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 +2235,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 +2632,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 + * @deprecated 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..87635f40b9 --- /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. + */ +public final 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/CaseDeleteAction.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseDeleteAction.java index 8a9f7bc17c..6ce1b421d5 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseDeleteAction.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseDeleteAction.java @@ -36,7 +36,7 @@ import org.sleuthkit.autopsy.coreutils.Logger; /** * The action associated with the Delete button of the Case Properties panel. It * deletes the current case. - * + * * This action should only be invoked in the event dispatch thread (EDT). */ final class CaseDeleteAction extends CallableSystemAction { @@ -90,7 +90,7 @@ final class CaseDeleteAction extends CallableSystemAction { logger.log(Level.SEVERE, String.format("Failed to delete case %s at %s", caseName, caseDirectory), ex); JOptionPane.showMessageDialog( null, - Bundle.Case_deleteCaseFailureMessageBox_message(ex.getMessage()), + Bundle.Case_deleteCaseFailureMessageBox_message(ex.getLocalizedMessage()), Bundle.Case_deleteCaseFailureMessageBox_title(), JOptionPane.ERROR_MESSAGE); } @@ -99,6 +99,7 @@ final class CaseDeleteAction extends CallableSystemAction { * of the Delete button that invokes this action. */ CasePropertiesAction.closeCasePropertiesWindow(); + StartupWindowProvider.getInstance().open(); } }.execute(); } 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..533cab58c5 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java @@ -115,26 +115,19 @@ public final class CaseOpenAction extends CallableSystemAction implements Action protected void done() { try { get(); - } catch (InterruptedException 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.getMessage(), - NbBundle.getMessage(this.getClass(), "CaseOpenAction.msgDlg.cantOpenCase.title"), //NON-NLS - JOptionPane.ERROR_MESSAGE); + } catch (InterruptedException | ExecutionException ex) { + if (ex instanceof InterruptedException || (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 + 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(); - } catch (ExecutionException ex) { - logger.log(Level.SEVERE, String.format("Error opening case with metadata file path %s", path), ex); //NON-NLS + } finally { 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)); } }.execute(); } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CasePropertiesAction.java b/Core/src/org/sleuthkit/autopsy/casemodule/CasePropertiesAction.java index 3f80544101..451a377a2f 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,34 +42,29 @@ 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) -> { + setEnabled(null != evt.getNewValue()); }); } @Override public void performAction() { SwingUtilities.invokeLater(() -> { - if (null == casePropertiesDialog) { - String title = NbBundle.getMessage(this.getClass(), "CasePropertiesAction.window.title"); - casePropertiesDialog = new JDialog(WindowManager.getDefault().getMainWindow(), title, false); - CaseInformationPanel caseInformationPanel = new CaseInformationPanel(); - caseInformationPanel.addCloseButtonAction((ActionEvent e) -> { - casePropertiesDialog.setVisible(false); - }); - casePropertiesDialog.add(caseInformationPanel); - casePropertiesDialog.setResizable(true); - casePropertiesDialog.pack(); + String title = NbBundle.getMessage(this.getClass(), "CasePropertiesAction.window.title"); + casePropertiesDialog = new JDialog(WindowManager.getDefault().getMainWindow(), title, false); + CaseInformationPanel caseInformationPanel = new CaseInformationPanel(); + caseInformationPanel.addCloseButtonAction((ActionEvent e) -> { + casePropertiesDialog.setVisible(false); + }); + casePropertiesDialog.add(caseInformationPanel); + casePropertiesDialog.setResizable(true); + casePropertiesDialog.pack(); - Dimension screenDimension = Toolkit.getDefaultToolkit().getScreenSize(); - double w = casePropertiesDialog.getSize().getWidth(); - double h = casePropertiesDialog.getSize().getHeight(); - casePropertiesDialog.setLocation((int) ((screenDimension.getWidth() - w) / 2), (int) ((screenDimension.getHeight() - h) / 2)); - casePropertiesDialog.setVisible(true); - } + Dimension screenDimension = Toolkit.getDefaultToolkit().getScreenSize(); + double w = casePropertiesDialog.getSize().getWidth(); + double h = casePropertiesDialog.getSize().getHeight(); + casePropertiesDialog.setLocation((int) ((screenDimension.getWidth() - w) / 2), (int) ((screenDimension.getHeight() - h) / 2)); + casePropertiesDialog.setVisible(true); casePropertiesDialog.setVisible(true); casePropertiesDialog.toFront(); }); @@ -87,6 +81,9 @@ final class CasePropertiesAction extends CallableSystemAction { } static void closeCasePropertiesWindow() { - casePropertiesDialog.setVisible(false); + if (null != casePropertiesDialog) { + casePropertiesDialog.setVisible(false); + casePropertiesDialog = null; + } } } 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/ImageDSProcessor.java b/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java index 36abbd307a..6a48384416 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java @@ -33,7 +33,7 @@ import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgress import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; import org.sleuthkit.autopsy.coreutils.DataSourceUtils; -import org.sleuthkit.autopsy.framework.AutoIngestDataSourceProcessor; +import org.sleuthkit.autopsy.datasourceprocessors.AutoIngestDataSourceProcessor; /** * A image file data source processor that implements the DataSourceProcessor diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskDSProcessor.java b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskDSProcessor.java index f19ad01af1..26185cb9ba 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskDSProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskDSProcessor.java @@ -31,7 +31,7 @@ import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgress import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; import org.sleuthkit.autopsy.coreutils.DriveUtils; import org.sleuthkit.autopsy.imagewriter.ImageWriterSettings; -import org.sleuthkit.autopsy.framework.AutoIngestDataSourceProcessor; +import org.sleuthkit.autopsy.datasourceprocessors.AutoIngestDataSourceProcessor; /** * A local drive data source processor that implements the DataSourceProcessor diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesDSProcessor.java b/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesDSProcessor.java index e207734714..974244bd30 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesDSProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesDSProcessor.java @@ -29,7 +29,7 @@ import org.openide.util.lookup.ServiceProviders; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; -import org.sleuthkit.autopsy.framework.AutoIngestDataSourceProcessor; +import org.sleuthkit.autopsy.datasourceprocessors.AutoIngestDataSourceProcessor; /** * A local/logical files and/or directories data source processor that 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 { try { - Case.openAsCurrentCase(casePath); + Case.openAsCurrentCase(caseMetadataFilePath); } catch (CaseActionException ex) { - logger.log(Level.SEVERE, String.format("Error opening case with metadata file path %s", casePath), ex); //NON-NLS SwingUtilities.invokeLater(() -> { - 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 (!(ex 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(OpenRecentCasePanel.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..5237c90487 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/RecentItems.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/RecentItems.java @@ -64,14 +64,17 @@ class RecentItems implements ActionListener { new Thread(() -> { try { Case.openAsCurrentCase(caseMetaDataFilePath); + StartupWindowProvider.getInstance().close(); } 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 (!(ex 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(); }); } 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/corecomponents/DataResultViewerTable.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java index a0c2635874..81d68c3029 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java @@ -26,9 +26,12 @@ import java.awt.Graphics; import java.awt.dnd.DnDConstants; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.beans.FeatureDescriptor; +import java.beans.PropertyChangeEvent; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -36,6 +39,8 @@ import java.util.Set; import java.util.TreeMap; import java.util.prefs.Preferences; import java.util.stream.Stream; +import java.util.TreeSet; +import java.util.prefs.Preferences; import javax.swing.JTable; import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; @@ -44,6 +49,8 @@ import javax.swing.event.ListSelectionEvent; import javax.swing.event.TableColumnModelEvent; import javax.swing.event.TableColumnModelListener; import javax.swing.table.TableCellRenderer; + +import javax.swing.table.TableColumnModel; import org.netbeans.swing.etable.ETableColumn; import org.netbeans.swing.outline.DefaultOutlineCellRenderer; import org.netbeans.swing.outline.DefaultOutlineModel; @@ -96,6 +103,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { // the column started and where it ended up. private int startColumnIndex = -1; private int endColumnIndex = -1; + private OutlineView ov; /* * Convience reference to internal Outline. @@ -125,8 +133,8 @@ public class DataResultViewerTable extends AbstractDataResultViewer { initComponents(); outlineView.setAllowedDragActions(DnDConstants.ACTION_NONE); - outline = outlineView.getOutline(); + outline = outlineView.getOutline(); outline.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); outline.setRootVisible(false); // don't show the root node outline.setDragEnabled(false); @@ -334,7 +342,6 @@ public class DataResultViewerTable extends AbstractDataResultViewer { @Override @ThreadConfined(type = ThreadConfined.ThreadType.AWT) public void setNode(Node selectedNode) { - /* * The quick filter must be reset because when determining column width, * ETable.getRowCount is called, and the documentation states that quick @@ -406,14 +413,15 @@ public class DataResultViewerTable extends AbstractDataResultViewer { // Get the columns setup with respect to names and sortability String[] propStrings = new String[props.size() * 2]; for (int i = 0; i < props.size(); i++) { - props.get(i).setValue("ComparableColumnTTV", Boolean.TRUE); //NON-NLS + final Property prop = props.get(i); + prop.setValue("ComparableColumnTTV", Boolean.TRUE); //NON-NLS //First property column is sorted initially if (i == 0) { - props.get(i).setValue("TreeColumnTTV", Boolean.TRUE); // Identifies special property representing first (tree) column. NON-NLS - props.get(i).setValue("SortingColumnTTV", Boolean.TRUE); // TreeTableView should be initially sorted by this property column. NON-NLS + prop.setValue("TreeColumnTTV", Boolean.TRUE); // Identifies special property representing first (tree) column. NON-NLS + prop.setValue("SortingColumnTTV", Boolean.TRUE); // TreeTableView should be initially sorted by this property column. NON-NLS } - propStrings[2 * i] = props.get(i).getName(); - propStrings[2 * i + 1] = props.get(i).getDisplayName(); + propStrings[2 * i] = prop.getName(); + propStrings[2 * i + 1] = prop.getDisplayName(); } outlineView.setPropertyColumns(propStrings); @@ -474,25 +482,44 @@ public class DataResultViewerTable extends AbstractDataResultViewer { final Preferences preferences = NbPreferences.forModule(DataResultViewerTable.class); // Store the current order of the columns into settings for (Map.Entry> entry : propertiesMap.entrySet()) { - preferences.put(getColumnPositionKey(tfn, entry.getValue().getName()), String.valueOf(entry.getKey())); - } - int numCols = outline.getColumnModel().getColumnCount(); + preferences.put(getColumnPositionKey(tfn.getColumnOrderKey(), entry.getValue().getName()), String.valueOf(entry.getKey())); + } + final TableColumnModel columnModel = ov.getOutline().getColumnModel(); + + //store the sorting information + int numCols = columnModel.getColumnCount(); for (int i = 0; i < numCols; i++) { - ETableColumn etc = (ETableColumn) outline.getColumnModel().getColumn(i); + ETableColumn etc = (ETableColumn) columnModel.getColumn(i); + String columnName = ov.getOutline().getColumnName(i); if (etc.isSorted()) { - preferences.put(getColumnSortOrderKey(tfn, etc.getIdentifier().toString()), String.valueOf(etc.isAscending())); - preferences.put(getColumnSortRankKey(tfn, etc.getIdentifier().toString()), String.valueOf(etc.getSortRank())); - preferences.put(getColumnSortOrderKey(tfn, etc.getIdentifier().toString()), String.valueOf(etc.isAscending())); - preferences.put(getColumnSortRankKey(tfn, etc.getIdentifier().toString()), String.valueOf(etc.getSortRank())); + preferences.put(getColumnSortOrderKey(tfn.getColumnOrderKey(), columnName), String.valueOf(etc.isAscending())); + preferences.put(getColumnSortRankKey(tfn.getColumnOrderKey(), columnName), String.valueOf(etc.getSortRank())); } else { - preferences.remove(getColumnSortOrderKey(tfn, etc.getIdentifier().toString())); - preferences.remove(getColumnSortRankKey(tfn, etc.getIdentifier().toString())); + preferences.remove(getColumnSortOrderKey(tfn.getColumnOrderKey(), columnName)); + preferences.remove(getColumnSortRankKey(tfn.getColumnOrderKey(), columnName)); } } } } + static private final class ColumnSortInfo { + + private final int modelIndex; + private final int rank; + private final boolean order; + + private ColumnSortInfo(int modelIndex, int rank, boolean order) { + this.modelIndex = modelIndex; + this.rank = rank; + this.order = order; + } + + private int getRank() { + return rank; + } + } + private synchronized void loadSort() { if (currentRoot == null || propertiesMap.isEmpty()) { return; @@ -502,23 +529,29 @@ public class DataResultViewerTable extends AbstractDataResultViewer { final Preferences preferences = NbPreferences.forModule(DataResultViewerTable.class); TableFilterNode tfn = (TableFilterNode) currentRoot; - Map orderMap = new TreeMap<>(); - Map indexMap = new TreeMap<>(); + if (currentRoot instanceof TableFilterNode) { + final String columnOrderKey = ((TableFilterNode) currentRoot).getColumnOrderKey(); - propertiesMap.entrySet().forEach((entry) -> { - Boolean sortOrder = Boolean.valueOf(preferences.get(getColumnSortOrderKey(tfn, entry.getValue().getName()), "true")); - Integer sortRank = Integer.valueOf(preferences.get(getColumnSortRankKey(tfn, entry.getValue().getName()), "0")); - //if the sort rank is undefined, it will be defaulted to 0 , unsorted. + final Preferences preferences = NbPreferences.forModule(DataResultViewerTable.class); - orderMap.put(sortRank, sortOrder); - indexMap.put(sortRank, outline.getColumn(entry.getValue().getName()).getModelIndex()); - }); + //organize property sorting information, sorted by rank + TreeSet treeSet = new TreeSet<>(Comparator.comparing(ColumnSortInfo::getRank)); + propertiesMap.entrySet().stream() + .forEach(entry -> { + final String propName = entry.getValue().getName(); + //if the sort rank is undefined, it will be defaulted to 0 => unsorted. + Integer sortRank = Integer.valueOf(preferences.get(getColumnSortRankKey(columnOrderKey, propName), "0")); + //default to true => ascending + Boolean sortOrder = Boolean.valueOf(preferences.get(getColumnSortOrderKey(columnOrderKey, propName), "true")); + + treeSet.add(new ColumnSortInfo(entry.getKey(), sortRank, sortOrder)); + }); + + //apply sort information in rank order. + treeSet.forEach(sortInfo -> ov.getOutline().setColumnSorted(sortInfo.modelIndex, sortInfo.order, sortInfo.rank)); + } - orderMap.entrySet().forEach((entry) -> { - outline.setColumnSorted(indexMap.get(entry.getKey()), orderMap.get(entry.getKey()), entry.getKey()); - }); } - } /** @@ -547,8 +580,9 @@ public class DataResultViewerTable extends AbstractDataResultViewer { boolean noPreviousSettings = true; final Preferences preferences = NbPreferences.forModule(DataResultViewerTable.class); + for (Property prop : props) { - Integer value = Integer.valueOf(preferences.get(getColumnPositionKey(tfn, prop.getName()), "-1")); + Integer value = Integer.valueOf(preferences.get(getColumnPositionKey(tfn.getColumnOrderKey(), prop.getName()), "-1")); if (value >= 0 && value < offset && !propertiesMap.containsKey(value)) { propertiesMap.put(value, prop); noPreviousSettings = false; @@ -574,29 +608,29 @@ public class DataResultViewerTable extends AbstractDataResultViewer { * Gets a key for the current node and a property of its child nodes to * store the column position into a preference file. * - * @param propName Property of the column - * @param node The current node to use as a base for the property key. + * @param prop Property of the column + * @param type The type of the current node * * @return A generated key for the preference file */ - private String getColumnPositionKey(TableFilterNode node, String propName) { - return getColumnKeyBase(node, propName) + ".column"; //NON-NLS + private String getColumnPositionKey(String type, String propName) { + return getColumnKeyBase(type, propName) + ".column"; } - private String getColumnSortOrderKey(TableFilterNode node, String propName) { - return getColumnKeyBase(node, propName) + ".sortOrder";//NON-NLS + private String getColumnSortOrderKey(String type, String propName) { + return getColumnKeyBase(type, propName) + ".sortOrder"; } - private String getColumnSortRankKey(TableFilterNode node, String propName) { - return getColumnKeyBase(node, propName) + ".sortRank";//NON-NLS + private String getColumnSortRankKey(String type, String propName) { + return getColumnKeyBase(type, propName) + ".sortRank"; } - private static String getColumnKeyBase(TableFilterNode node, String propName) { - return stripNonAlphanumeric(node.getColumnOrderKey()) + "." + stripNonAlphanumeric(propName); + private static String getColumnKeyBase(String type, String propName) { + return stripNonAlphanumeric(type) + "." + stripNonAlphanumeric(propName); } private static String stripNonAlphanumeric(String str) { - return str.replaceAll("[^a-zA-Z0-9_]", "");//NON-NLS + return str.replaceAll("[^a-zA-Z0-9_]", ""); } @Override diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNodeVisitor.java b/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNodeVisitor.java index 972951ea99..5cc152e465 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNodeVisitor.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNodeVisitor.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"); @@ -90,7 +90,7 @@ public interface DisplayableItemNodeVisitor { T visit(KeywordHits.ListNode khsn); T visit(KeywordHits.TermNode khmln); - + T visit(KeywordHits.RegExpInstanceNode khmln); T visit(HashsetHits.RootNode hhrn); @@ -156,6 +156,8 @@ public interface DisplayableItemNodeVisitor { T visit(EmptyNode.MessageNode emptyNode); + T visit(InterestingHits.InterestingItemTypeNode aThis); + /** * Visitor with an implementable default behavior for all types. Override * specific visit types to not use the default behavior. @@ -238,6 +240,11 @@ public interface DisplayableItemNodeVisitor { return defaultVisit(ftByMimeTypeEmptyNode); } + @Override + public T visit(InterestingHits.InterestingItemTypeNode interestingItemTypeNode) { + return defaultVisit(interestingItemTypeNode); + } + @Override public T visit(DeletedContentNode dcn) { return defaultVisit(dcn); @@ -282,7 +289,7 @@ public interface DisplayableItemNodeVisitor { public T visit(KeywordHits.ListNode khsn) { return defaultVisit(khsn); } - + @Override public T visit(KeywordHits.RegExpInstanceNode khsn) { return defaultVisit(khsn); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByMimeType.java b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByMimeType.java index db191ac76c..1549d6bdc1 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByMimeType.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByMimeType.java @@ -453,7 +453,7 @@ public final class FileTypesByMimeType extends Observable implements AutopsyVisi * @return query.toString - portion of SQL query which will follow a * WHERE clause. */ - private String createQuery(String mime_type) { + private String createQuery(String mimeType) { StringBuilder query = new StringBuilder(); query.append("(dir_type = ").append(TskData.TSK_FS_NAME_TYPE_ENUM.REG.getValue()).append(")"); //NON-NLS query.append(" AND (type IN (").append(TskData.TSK_DB_FILES_TYPE_ENUM.FS.ordinal()).append(","); //NON-NLS @@ -466,7 +466,7 @@ public final class FileTypesByMimeType extends Observable implements AutopsyVisi if (UserPreferences.hideKnownFilesInViewsTree()) { query.append(" AND (known IS NULL OR known != ").append(TskData.FileKnown.KNOWN.getFileKnownValue()).append(")"); //NON-NLS } - query.append(" AND mime_type = '").append(mime_type).append("'"); //NON-NLS + query.append(" AND mime_type = '").append(mimeType).append("'"); //NON-NLS return query.toString(); } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java b/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java index db409d7ec9..2665894481 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java @@ -379,9 +379,10 @@ public class HashsetHits implements AutopsyVisitableItem { hashsetResults.getArtifactIds(hashsetName).forEach((id) -> { try { - BlackboardArtifact art = skCase.getBlackboardArtifact(id); - - artifactHits.put(id, art); + if (!artifactHits.containsKey(id)) { + BlackboardArtifact art = skCase.getBlackboardArtifact(id); + artifactHits.put(id, art); + } list.add(id); } catch (TskException ex) { logger.log(Level.SEVERE, "TSK Exception occurred", ex); //NON-NLS diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/InterestingHits.java b/Core/src/org/sleuthkit/autopsy/datamodel/InterestingHits.java index e6a111a5d8..d9fecdcdb2 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/InterestingHits.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/InterestingHits.java @@ -1,15 +1,15 @@ /* * Autopsy Forensic Browser - * - * Copyright 2011-2015 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. @@ -66,20 +66,20 @@ public class InterestingHits implements AutopsyVisitableItem { private class InterestingResults extends Observable { // NOTE: the map can be accessed by multiple worker threads and needs to be synchronized - private final Map> interestingItemsMap = new LinkedHashMap<>(); + private final Map>> interestingItemsMap = new LinkedHashMap<>(); public List getSetNames() { List setNames; synchronized (interestingItemsMap) { setNames = new ArrayList<>(interestingItemsMap.keySet()); - } + } Collections.sort(setNames); return setNames; } - public Set getArtifactIds(String setName) { + public Set getArtifactIds(String setName, String typeName) { synchronized (interestingItemsMap) { - return interestingItemsMap.get(setName); + return interestingItemsMap.get(setName).get(typeName); } } @@ -118,9 +118,11 @@ public class InterestingHits implements AutopsyVisitableItem { String value = resultSet.getString("value_text"); //NON-NLS long artifactId = resultSet.getLong("artifact_id"); //NON-NLS if (!interestingItemsMap.containsKey(value)) { - interestingItemsMap.put(value, new HashSet<>()); + interestingItemsMap.put(value, new LinkedHashMap<>()); + interestingItemsMap.get(value).put(BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getDisplayName(), new HashSet<>()); + interestingItemsMap.get(value).put(BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_ARTIFACT_HIT.getDisplayName(), new HashSet<>()); } - interestingItemsMap.get(value).add(artifactId); + interestingItemsMap.get(value).get(artType.getDisplayName()).add(artifactId); } } } catch (TskCoreException | SQLException ex) { @@ -280,7 +282,7 @@ public class InterestingHits implements AutopsyVisitableItem { private final String setName; public SetNameNode(String setName) {//, Set children) { - super(Children.create(new HitFactory(setName), true), Lookups.singleton(setName)); + super(Children.create(new HitTypeFactory(setName), true), Lookups.singleton(setName)); this.setName = setName; super.setName(setName); updateDisplayName(); @@ -289,12 +291,14 @@ public class InterestingHits implements AutopsyVisitableItem { } private void updateDisplayName() { - super.setDisplayName(setName + " (" + interestingResults.getArtifactIds(setName).size() + ")"); + int sizeOfSet = interestingResults.getArtifactIds(setName, BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_ARTIFACT_HIT.getDisplayName()).size() + + interestingResults.getArtifactIds(setName, BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getDisplayName()).size(); + super.setDisplayName(setName + " (" + sizeOfSet + ")"); } @Override public boolean isLeafTypeNode() { - return true; + return false; } @Override @@ -334,29 +338,120 @@ public class InterestingHits implements AutopsyVisitableItem { } } - private class HitFactory extends ChildFactory implements Observer { + private class HitTypeFactory extends ChildFactory implements Observer { private final String setName; - private Map artifactHits = new HashMap<>(); + private final Map artifactHits = new HashMap<>(); - private HitFactory(String setName) { + private HitTypeFactory(String setName) { super(); this.setName = setName; interestingResults.addObserver(this); } @Override - protected boolean createKeys(List list) { - - if (skCase == null) { - return true; + protected boolean createKeys(List list) { + list.add(BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getDisplayName()); + list.add(BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_ARTIFACT_HIT.getDisplayName()); + return true; + } + + @Override + protected Node createNodeForKey(String key) { + return new InterestingItemTypeNode(setName, key); + } + + @Override + public void update(Observable o, Object arg) { + refresh(true); + } + } + + public class InterestingItemTypeNode extends DisplayableItemNode implements Observer { + + private final String typeName; + private final String setName; + + private InterestingItemTypeNode(String setName, String typeName) { + super(Children.create(new HitFactory(setName, typeName), true), Lookups.singleton(setName)); + this.typeName = typeName; + this.setName = setName; + super.setName(typeName); + updateDisplayName(); + this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/interesting_item.png"); //NON-NLS + interestingResults.addObserver(this); + } + + private void updateDisplayName() { + super.setDisplayName(typeName + " (" + interestingResults.getArtifactIds(setName, typeName).size() + ")"); + } + + @Override + public boolean isLeafTypeNode() { + return true; + } + + @Override + protected Sheet createSheet() { + Sheet s = super.createSheet(); + Sheet.Set ss = s.get(Sheet.PROPERTIES); + if (ss == null) { + ss = Sheet.createPropertiesSet(); + s.put(ss); } - - interestingResults.getArtifactIds(setName).forEach((id) -> { + ss.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "InterestingHits.createSheet.name.name"), + NbBundle.getMessage(this.getClass(), "InterestingHits.createSheet.name.name"), + NbBundle.getMessage(this.getClass(), "InterestingHits.createSheet.name.desc"), + getName())); + return s; + } + + @Override + public T accept(DisplayableItemNodeVisitor v) { + return v.visit(this); + } + + @Override + public void update(Observable o, Object arg) { + updateDisplayName(); + } + + @Override + public String getItemType() { + /** + * For custom settings for each rule set, return + * getClass().getName() + setName instead. + */ + return getClass().getName(); + } + } + + private class HitFactory extends ChildFactory implements Observer { + + private final String setName; + private final String typeName; + private final Map artifactHits = new HashMap<>(); + + private HitFactory(String setName, String typeName) { + super(); + this.setName = setName; + this.typeName = typeName; + interestingResults.addObserver(this); + } + + @Override + protected boolean createKeys(List list) { + + if (skCase == null) { + return true; + } + + interestingResults.getArtifactIds(setName, typeName).forEach((id) -> { try { - BlackboardArtifact art = skCase.getBlackboardArtifact(id); - - artifactHits.put(id, art); + if (!artifactHits.containsKey(id)) { + BlackboardArtifact art = skCase.getBlackboardArtifact(id); + artifactHits.put(id, art); + } list.add(id); } catch (TskCoreException ex) { logger.log(Level.SEVERE, "TSK Exception occurred", ex); //NON-NLS diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/KeywordHits.java b/Core/src/org/sleuthkit/autopsy/datamodel/KeywordHits.java index 197c893575..ccc2b3c9e7 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/KeywordHits.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/KeywordHits.java @@ -707,14 +707,17 @@ public class KeywordHits implements AutopsyVisitableItem { if ((instances.size() == 1) && (instances.get(0).equals(DEFAULT_INSTANCE_NAME))) { for (Long id : keywordResults.getArtifactIds(setName, keyword, DEFAULT_INSTANCE_NAME) ) { RegExpInstanceKey key = new RegExpInstanceKey(id); - nodesMap.put(key, createNode(key)); + if (!nodesMap.containsKey(key)) { + nodesMap.put(key, createNode(key)); + } list.add(key); - } } else { for (String instance : instances) { RegExpInstanceKey key = new RegExpInstanceKey(instance); - nodesMap.put(key, createNode(key)); + if (!nodesMap.containsKey(key)) { + nodesMap.put(key, createNode(key)); + } list.add(key); } @@ -897,11 +900,12 @@ public class KeywordHits implements AutopsyVisitableItem { @Override protected boolean createKeys(List list) { - list.addAll(keywordResults.getArtifactIds(setName, keyword, instance)); for (Long id : keywordResults.getArtifactIds(setName, keyword, instance) ) { + if (!nodesMap.containsKey(id)) { nodesMap.put(id, createBlackboardArtifactNode(id)); - list.add(id); } + list.add(id); + } return true; } diff --git a/Core/src/org/sleuthkit/autopsy/framework/AutoIngestDataSourceProcessor.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/AutoIngestDataSourceProcessor.java similarity index 98% rename from Core/src/org/sleuthkit/autopsy/framework/AutoIngestDataSourceProcessor.java rename to Core/src/org/sleuthkit/autopsy/datasourceprocessors/AutoIngestDataSourceProcessor.java index e4acab8234..11c0559795 100644 --- a/Core/src/org/sleuthkit/autopsy/framework/AutoIngestDataSourceProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/AutoIngestDataSourceProcessor.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.framework; +package org.sleuthkit.autopsy.datasourceprocessors; import java.nio.file.Path; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/RawDSProcessor.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/RawDSProcessor.java index 68908ae7a3..5b9edb5a83 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/RawDSProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/RawDSProcessor.java @@ -1,15 +1,15 @@ /* * Autopsy Forensic Browser - * + * * Copyright 2011-2016 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. @@ -28,26 +28,27 @@ import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; /** * A Raw data source processor that implements the DataSourceProcessor service - * provider interface to allow integration with the add data source wizard. - * It also provides a run method overload to allow it to be used independently - * of the wizard. + * provider interface to allow integration with the add data source wizard. It + * also provides a run method overload to allow it to be used independently of + * the wizard. */ @ServiceProvider(service = DataSourceProcessor.class) public class RawDSProcessor implements DataSourceProcessor { + private final RawDSInputPanel configPanel; private AddRawImageTask addImageTask; /* * Constructs a Raw data source processor that implements the - * DataSourceProcessor service provider interface to allow integration - * with the add data source wizard. It also provides a run method - * overload to allow it to be used independently of the wizard. + * DataSourceProcessor service provider interface to allow integration with + * the add data source wizard. It also provides a run method overload to + * allow it to be used independently of the wizard. */ public RawDSProcessor() { configPanel = RawDSInputPanel.createInstance(RawDSProcessor.class.getName()); } -/** + /** * Gets a string that describes the type of data sources this processor is * able to add to the case database. The string is suitable for display in a * type selection UI component (e.g., a combo box). @@ -68,7 +69,7 @@ public class RawDSProcessor implements DataSourceProcessor { */ @Override public String getDataSourceType() { - return Bundle.RawDSProcessor_dataSourceType(); + return Bundle.RawDSProcessor_dataSourceType(); } /** @@ -115,32 +116,36 @@ public class RawDSProcessor implements DataSourceProcessor { @Override public void run(DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { configPanel.storeSettings(); - run(UUID.randomUUID().toString(), configPanel.getImageFilePath(), configPanel.getTimeZone(), configPanel.getChunkSize(), progressMonitor, callback); + run(UUID.randomUUID().toString(), configPanel.getImageFilePath(), configPanel.getTimeZone(), configPanel.getChunkSize(), progressMonitor, callback); } /** - * Adds a data source to the case database using a background task in a - * separate thread and the given settings instead of those provided by the + * Adds a "raw" data source to the case database using a background task in + * a separate thread and the given settings instead of those provided by the * selection and configuration panel. Returns as soon as the background task * is started and uses the callback object to signal task completion and * return results. * - * @param deviceId An ASCII-printable identifier for the - * device associated with the data source - * that is intended to be unique across - * multiple cases (e.g., a UUID). - * @param rawDSInputFilePath Path to a Raw data source file. - * @param isHandsetFile Indicates whether the XML file is for a - * handset or a SIM. - * @param progressMonitor Progress monitor for reporting progress - * during processing. + * @param deviceId An ASCII-printable identifier for the device + * associated with the data source that is + * intended to be unique across multiple cases + * (e.g., a UUID). + * @param imageFilePath Path to the image file. + * @param timeZone The time zone to use when processing dates + * and times for the image, obtained from + * java.util.TimeZone.getID. + * @param chunkSize The maximum size of each chunk of the raw + * data source as it is divided up into virtual + * unallocated space files. + * @param progressMonitor Progress monitor for reporting progress + * during processing. + * @param callback Callback to call when processing is done. */ private void run(String deviceId, String imageFilePath, String timeZone, long chunkSize, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { addImageTask = new AddRawImageTask(deviceId, imageFilePath, timeZone, chunkSize, progressMonitor, callback); new Thread(addImageTask).start(); } - @Override public void cancel() { } diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java b/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java index 58cba4102f..0d853e09cc 100755 --- a/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java @@ -71,11 +71,12 @@ import org.sleuthkit.datamodel.TskException; import org.sleuthkit.datamodel.VirtualDirectory; /** - * This class wraps nodes as they are passed to the DataResult viewers. It - * defines the actions that the node should have. + * A node used to wrap another node before passing it to the result viewers. The + * wrapper node defines the actions associated with the wrapped node and may + * filter out some of its children. */ public class DataResultFilterNode extends FilterNode { - + private static final Logger LOGGER = Logger.getLogger(DataResultFilterNode.class.getName()); private static boolean filterKnownFromDataSources = UserPreferences.hideKnownFilesInDataSourcesTree(); @@ -111,22 +112,33 @@ public class DataResultFilterNode extends FilterNode { private final ExplorerManager sourceEm; /** + * Constructs a node used to wrap another node before passing it to the + * result viewers. The wrapper node defines the actions associated with the + * wrapped node and may filter out some of its children. * - * @param node Root node to be passed to DataResult viewers - * @param em ExplorerManager for component that is creating the node + * @param node The node to wrap. + * @param em The ExplorerManager for the component that is creating the + * node. */ public DataResultFilterNode(Node node, ExplorerManager em) { super(node, new DataResultFilterChildren(node, em)); this.sourceEm = em; - } /** + * Constructs a node used to wrap another node before passing it to the + * result viewers. The wrapper node defines the actions associated with the + * wrapped node and may filter out some of its children. * - * @param node Root node to be passed to DataResult viewers - * @param em ExplorerManager for component that is creating the node + * @param node The node to wrap. + * @param em The ExplorerManager for the component that is creating + * the node. + * @param filterKnown Whether or not to filter out children that represent + * known files. + * @param filterSlack Whether or not to filter out children that represent + * virtual slack space files. */ - private DataResultFilterNode(Node node, ExplorerManager em, boolean filterKnown, boolean filterSlack) { + private DataResultFilterNode(Node node, ExplorerManager em, boolean filterKnown, boolean filterSlack) { super(node, new DataResultFilterChildren(node, em, filterKnown, filterSlack)); this.sourceEm = em; } @@ -201,9 +213,9 @@ public class DataResultFilterNode extends FilterNode { * DataResultFilterNode that created in the DataResultFilterNode.java. * */ - private static class DataResultFilterChildren extends FilterNode.Children { + private static class DataResultFilterChildren extends FilterNode.Children { - private final ExplorerManager sourceEm; + private final ExplorerManager sourceEm; private boolean filterKnown; private boolean filterSlack; @@ -211,7 +223,7 @@ public class DataResultFilterNode extends FilterNode { /** * the constructor */ - private DataResultFilterChildren(Node arg, ExplorerManager sourceEm) { + private DataResultFilterChildren(Node arg, ExplorerManager sourceEm) { super(arg); switch (SelectionContext.getSelectionContext(arg)) { case DATA_SOURCES: @@ -230,7 +242,7 @@ public class DataResultFilterNode extends FilterNode { this.sourceEm = sourceEm; } - private DataResultFilterChildren(Node arg, ExplorerManager sourceEm, boolean filterKnown, boolean filterSlack) { + private DataResultFilterChildren(Node arg, ExplorerManager sourceEm, boolean filterKnown, boolean filterSlack) { super(arg); this.filterKnown = filterKnown; this.filterSlack = filterSlack; @@ -325,10 +337,10 @@ public class DataResultFilterNode extends FilterNode { actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); actionsList.add(AddBlackboardArtifactTagAction.getInstance()); - - final Collection selectedFilesList = - new HashSet<>(Utilities.actionsGlobalContext().lookupAll(AbstractFile.class)); - if(selectedFilesList.size() == 1) { + + final Collection selectedFilesList + = new HashSet<>(Utilities.actionsGlobalContext().lookupAll(AbstractFile.class)); + if (selectedFilesList.size() == 1) { actionsList.add(DeleteFileContentTagAction.getInstance()); } } else { @@ -337,17 +349,17 @@ public class DataResultFilterNode extends FilterNode { actionsList.add(null); actionsList.add(AddBlackboardArtifactTagAction.getInstance()); } - - final Collection selectedArtifactsList = - new HashSet<>(Utilities.actionsGlobalContext().lookupAll(BlackboardArtifact.class)); - if(selectedArtifactsList.size() == 1) { + + final Collection selectedArtifactsList + = new HashSet<>(Utilities.actionsGlobalContext().lookupAll(BlackboardArtifact.class)); + if (selectedArtifactsList.size() == 1) { actionsList.add(DeleteFileBlackboardArtifactTagAction.getInstance()); } - - if(n != null) { + + if (n != null) { actionsList.addAll(ContextMenuExtensionPoint.getActions()); } - + return actionsList; } @@ -361,8 +373,7 @@ public class DataResultFilterNode extends FilterNode { public List visit(FileTypesNode fileTypes) { return defaultVisit(fileTypes); } - - + @Override protected List defaultVisit(DisplayableItemNode ditem) { //preserve the default node's actions @@ -454,14 +465,12 @@ public class DataResultFilterNode extends FilterNode { protected AbstractAction defaultVisit(DisplayableItemNode c) { return openChild(c); } - + @Override public AbstractAction visit(FileTypesNode fileTypes) { return openChild(fileTypes); } - - /** * Tell the originating ExplorerManager to display the given * dataModelNode. diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ViewContextAction.java b/Core/src/org/sleuthkit/autopsy/directorytree/ViewContextAction.java index ce4c0a4d92..b674d50bef 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/ViewContextAction.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ViewContextAction.java @@ -1,15 +1,15 @@ /* * Autopsy Forensic Browser - * - * Copyright 2011 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. @@ -56,15 +56,15 @@ import org.sleuthkit.datamodel.VolumeSystem; * the DataResultViewer. 3. Waits for all the Children to be contentNode in the * DataResultViewer and selects the Node that represents the Content. */ -public class ViewContextAction extends AbstractAction { +public final class ViewContextAction extends AbstractAction { - private Content content; + private static final long serialVersionUID = 1L; private static final Logger logger = Logger.getLogger(ViewContextAction.class.getName()); + private Content content; public ViewContextAction(String title, BlackboardArtifactNode node) { super(title); this.content = node.getLookup().lookup(Content.class); - } public ViewContextAction(String title, AbstractFsContentNode node) { @@ -79,65 +79,70 @@ public class ViewContextAction extends AbstractAction { @Override public void actionPerformed(ActionEvent e) { - EventQueue.invokeLater(new Runnable() { - @Override - public void run() { - // create a list of Content objects starting with content's - // Image and ends with content - ReverseHierarchyVisitor vtor = new ReverseHierarchyVisitor(); - List hierarchy = content.accept(vtor); - Collections.reverse(hierarchy); + EventQueue.invokeLater(() -> { + // create a list of Content objects starting with content's + // Image and ends with content + ReverseHierarchyVisitor vtor = new ReverseHierarchyVisitor(); + List hierarchy = content.accept(vtor); + Collections.reverse(hierarchy); - Node generated = new DirectoryTreeFilterNode(new AbstractNode(new RootContentChildren(hierarchy)), true); - Children genChilds = generated.getChildren(); + Node generated = new DirectoryTreeFilterNode(new AbstractNode(new RootContentChildren(hierarchy)), true); + Children genChilds = generated.getChildren(); - final DirectoryTreeTopComponent dirTree = DirectoryTreeTopComponent.findInstance(); - TreeView dirTreeView = dirTree.getTree(); - ExplorerManager dirTreeExplorerManager = dirTree.getExplorerManager(); - Node dirTreeRootNode = dirTreeExplorerManager.getRootContext(); - Children dirChilds = dirTreeRootNode.getChildren(); - Children currentChildren = dirChilds.findChild(DataSourcesNode.NAME).getChildren(); + final DirectoryTreeTopComponent dirTree = DirectoryTreeTopComponent.findInstance(); + TreeView dirTreeView = dirTree.getTree(); + ExplorerManager dirTreeExplorerManager = dirTree.getExplorerManager(); + Node dirTreeRootNode = dirTreeExplorerManager.getRootContext(); + Children dirChilds = dirTreeRootNode.getChildren(); + Children currentChildren = dirChilds.findChild(DataSourcesNode.NAME).getChildren(); - Node dirExplored = null; + Node dirExplored = null; - // Find the parent node of the content in the directory tree - for (int i = 0; i < genChilds.getNodesCount() - 1; i++) { - Node currentGeneratedNode = genChilds.getNodeAt(i); - for (int j = 0; j < currentChildren.getNodesCount(); j++) { - Node currentDirectoryTreeNode = currentChildren.getNodeAt(j); - if (currentGeneratedNode.getDisplayName().equals(currentDirectoryTreeNode.getDisplayName())) { - dirExplored = currentDirectoryTreeNode; - dirTreeView.expandNode(dirExplored); - currentChildren = currentDirectoryTreeNode.getChildren(); - break; - } - } - } - - // Set the parent node of the content as the selection in the - // directory tree - try { - if (dirExplored != null) { + // Find the parent node of the content in the directory tree + for (int i = 0; i < genChilds.getNodesCount() - 1; i++) { + Node currentGeneratedNode = genChilds.getNodeAt(i); + for (int j = 0; j < currentChildren.getNodesCount(); j++) { + Node currentDirectoryTreeNode = currentChildren.getNodeAt(j); + if (currentGeneratedNode.getDisplayName().equals(currentDirectoryTreeNode.getDisplayName())) { + dirExplored = currentDirectoryTreeNode; dirTreeView.expandNode(dirExplored); - dirTreeExplorerManager.setExploredContextAndSelection(dirExplored, new Node[]{dirExplored}); + currentChildren = currentDirectoryTreeNode.getChildren(); + break; } - } catch (PropertyVetoException ex) { - logger.log(Level.WARNING, "Couldn't set selected node", ex); //NON-NLS } - - EventQueue.invokeLater(new Runnable() { - @Override - public void run() { - DataResultTopComponent dataResultTC = dirTree.getDirectoryListing(); - Node currentRootNodeOfDataResultTC = dataResultTC.getRootNode(); - Node contentNode = content.accept(new RootContentChildren.CreateSleuthkitNodeVisitor()); - new SelectionWorker(dataResultTC, contentNode.getName(), currentRootNodeOfDataResultTC).execute(); - } - }); } + + // Set the parent node of the content as the selection in the + // directory tree + try { + if (dirExplored != null) { + dirTreeView.expandNode(dirExplored); + dirTreeExplorerManager.setExploredContextAndSelection(dirExplored, new Node[]{dirExplored}); + } + } catch (PropertyVetoException ex) { + logger.log(Level.WARNING, "Couldn't set selected node", ex); //NON-NLS + } + + EventQueue.invokeLater(() -> { + DataResultTopComponent dataResultTC = dirTree.getDirectoryListing(); + Node currentRootNodeOfDataResultTC = dataResultTC.getRootNode(); + Node contentNode = content.accept(new RootContentChildren.CreateSleuthkitNodeVisitor()); + new SelectionWorker(dataResultTC, contentNode.getName(), currentRootNodeOfDataResultTC).execute(); + }); }); } + @Override + public Object clone() throws CloneNotSupportedException { + ViewContextAction clone = (ViewContextAction)super.clone(); + clone.setContent(this.content); + return clone; + } + + private void setContent(Content content) { + this.content = content; + } + /** * Waits for a Node's children to be generated, regardless of whether they * are lazily loaded, then sets the correct selection in a specified @@ -215,14 +220,14 @@ public class ViewContextAction extends AbstractAction { */ private class ReverseHierarchyVisitor extends ContentVisitor.Default> { - List ret = new ArrayList(); + List ret = new ArrayList<>(); private List visitParentButDontAddMe(Content content) { Content parent = null; try { parent = content.getParent(); } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Couldn't get parent of Content object: " + content); //NON-NLS + logger.log(Level.WARNING, "Could not get parent of Content object: {0}", content); //NON-NLS } return parent == null ? ret : parent.accept(this); } @@ -234,7 +239,7 @@ public class ViewContextAction extends AbstractAction { try { parent = content.getParent(); } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Couldn't get parent of Content object: " + content); //NON-NLS + logger.log(Level.WARNING, "Could not get parent of Content object: {0}", content); //NON-NLS } return parent == null ? ret : parent.accept(this); } diff --git a/Core/src/org/sleuthkit/autopsy/imagewriter/ImageWriterService.java b/Core/src/org/sleuthkit/autopsy/imagewriter/ImageWriterService.java index 594532cbc6..8ee1fed9eb 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,100 @@ 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)}) +import org.sleuthkit.autopsy.appservices.AutopsyService; /** - * 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 + * + * @param imageId ID for the image. + * @param settings Image writer settings to be used when writing 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/ingest/IngestJobSettings.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestJobSettings.java index 9fc5442371..ad93b012c3 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestJobSettings.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestJobSettings.java @@ -393,6 +393,9 @@ public class IngestJobSettings { /** * Gets the module names for a given key within these ingest job settings. * + * @param context The identifier for the context for which to get the + * module names, e.g., the Add Data Source wizard or + * Run Ingest Modules context. * @param key The key string. * @param defaultSetting The default list of module names. * diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestOptionsPanel.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestOptionsPanel.java index d10b987873..1c761e8578 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestOptionsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestOptionsPanel.java @@ -138,9 +138,6 @@ public class IngestOptionsPanel extends IngestModuleGlobalSettingsPanel implemen } - /** - * @inheritDoc - */ @Override public void addPropertyChangeListener(PropertyChangeListener l) { filterPanel.addPropertyChangeListener(l); @@ -148,9 +145,6 @@ public class IngestOptionsPanel extends IngestModuleGlobalSettingsPanel implemen profilePanel.addPropertyChangeListener(l); } - /** - * @inheritDoc - */ @Override public void removePropertyChangeListener(PropertyChangeListener l) { filterPanel.removePropertyChangeListener(l); @@ -158,9 +152,6 @@ public class IngestOptionsPanel extends IngestModuleGlobalSettingsPanel implemen profilePanel.removePropertyChangeListener(l); } - /** - * @inheritDoc - */ @Override public void saveSettings() { saveTabByIndex(tabbedPane.getSelectedIndex()); @@ -189,17 +180,11 @@ public class IngestOptionsPanel extends IngestModuleGlobalSettingsPanel implemen } } - /** - * @inheritDoc - */ @Override public void store() { saveSettings(); } - /** - * @inheritDoc - */ @Override public void load() { filterPanel.load(); diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestOptionsPanelController.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestOptionsPanelController.java index 9c908d9ebb..69c88d63c4 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestOptionsPanelController.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestOptionsPanelController.java @@ -40,9 +40,6 @@ public class IngestOptionsPanelController extends OptionsPanelController { private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); private boolean changed; - /** - * @inheritDoc - */ @Override public void update() { getPanel().load(); @@ -69,9 +66,6 @@ public class IngestOptionsPanelController extends OptionsPanelController { return panel; } - /** - * @inheritDoc - */ @Override public void applyChanges() { if (changed) { @@ -85,57 +79,36 @@ public class IngestOptionsPanelController extends OptionsPanelController { } } - /** - * @inheritDoc - */ @Override public void cancel() { getPanel().cancel(); } - /** - * @inheritDoc - */ @Override public boolean isValid() { return getPanel().valid(); } - /** - * @inheritDoc - */ @Override public boolean isChanged() { return changed; } - /** - * @inheritDoc - */ @Override public JComponent getComponent(Lookup lkp) { return getPanel(); } - /** - * @inheritDoc - */ @Override public HelpCtx getHelpCtx() { return null; } - /** - * @inheritDoc - */ @Override public void addPropertyChangeListener(PropertyChangeListener pl) { pcs.addPropertyChangeListener(pl); } - /** - * @inheritDoc - */ @Override public void removePropertyChangeListener(PropertyChangeListener pl) { pcs.removePropertyChangeListener(pl); diff --git a/Core/src/org/sleuthkit/autopsy/ingest/runIngestModuleWizard/ShortcutWizardDescriptorPanel.java b/Core/src/org/sleuthkit/autopsy/ingest/runIngestModuleWizard/ShortcutWizardDescriptorPanel.java index 589ebdee14..ddbe9cf81d 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/runIngestModuleWizard/ShortcutWizardDescriptorPanel.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/runIngestModuleWizard/ShortcutWizardDescriptorPanel.java @@ -43,7 +43,6 @@ public abstract class ShortcutWizardDescriptorPanel implements WizardDescriptor. /** * Whether or not the panel immediately following this one should be skipped - * . * * @return true or false */ diff --git a/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/ImageExtractor.java b/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/ImageExtractor.java index 9df9927ad8..d58d935e97 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/ImageExtractor.java +++ b/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/ImageExtractor.java @@ -21,19 +21,24 @@ package org.sleuthkit.autopsy.modules.embeddedfileextractor; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.lang.IllegalArgumentException; +import java.lang.IndexOutOfBoundsException; +import java.lang.NullPointerException; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; -import org.apache.poi.OldFileFormatException; import org.apache.poi.POIXMLException; -import org.apache.poi.hslf.model.Picture; -import org.apache.poi.hslf.usermodel.PictureData; -import org.apache.poi.hslf.usermodel.SlideShow; +import org.apache.poi.hwpf.usermodel.Picture; +import org.apache.poi.hslf.usermodel.HSLFPictureData; +import org.apache.poi.hslf.usermodel.HSLFSlideShow; +import org.apache.poi.hssf.record.RecordInputStream.LeftoverDataException; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.hwpf.HWPFDocument; import org.apache.poi.hwpf.model.PicturesTable; +import org.apache.poi.sl.usermodel.PictureData.PictureType; import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.util.RecordFormatException; import org.apache.poi.xslf.usermodel.XMLSlideShow; import org.apache.poi.xslf.usermodel.XSLFPictureData; import org.apache.poi.xssf.usermodel.XSSFWorkbook; @@ -209,20 +214,31 @@ class ImageExtractor { * extracted. */ private List extractImagesFromDoc(AbstractFile af) { - List listOfAllPictures; + List listOfAllPictures; try { HWPFDocument doc = new HWPFDocument(new ReadContentInputStream(af)); PicturesTable pictureTable = doc.getPicturesTable(); listOfAllPictures = pictureTable.getAllPictures(); - } catch (OldFileFormatException | IOException ex) { - // OldFileFormatException: - // Thrown when the document version is unsupported (Word 95 and - // older) - + } catch (IOException | IllegalArgumentException | + IndexOutOfBoundsException | NullPointerException ex) { // IOException: // Thrown when the document has issues being read. + // IllegalArgumentException: + // This will catch OldFileFormatException, which is thrown when the + // document's format is Word 95 or older. Alternatively, this is + // thrown when attempting to load an RTF file as a DOC file. + // However, our code verifies the file format before ever running it + // through the ImageExtractor. This exception gets thrown in the + // "IN10-0137.E01" image regardless. The reason is unknown. + + // IndexOutOfBoundsException: + // NullPointerException: + // These get thrown in certain images. The reason is unknown. It is + // likely due to problems with the file formats that POI is poorly + // handling. + return null; } catch (Throwable ex) { // instantiating POI containers throw RuntimeExceptions @@ -241,7 +257,7 @@ class ImageExtractor { } List listOfExtractedImages = new ArrayList<>(); byte[] data = null; - for (org.apache.poi.hwpf.usermodel.Picture picture : listOfAllPictures) { + for (Picture picture : listOfAllPictures) { String fileName = picture.suggestFullFileName(); try { data = picture.getContent(); @@ -319,17 +335,25 @@ class ImageExtractor { * extracted. */ private List extractImagesFromPpt(AbstractFile af) { - PictureData[] listOfAllPictures = null; + List listOfAllPictures = null; try { - SlideShow ppt = new SlideShow(new ReadContentInputStream(af)); + HSLFSlideShow ppt = new HSLFSlideShow(new ReadContentInputStream(af)); listOfAllPictures = ppt.getPictureData(); - } catch (OldFileFormatException | IOException ex) { - // OldFileFormatException: - // Thrown when the document version is unsupported + } catch (IOException | IllegalArgumentException | + IndexOutOfBoundsException ex) { + // IllegalArgumentException: + // This will catch OldFileFormatException, which is thrown when the + // document version is unsupported. The IllegalArgumentException may + // also get thrown for unknown reasons. // IOException: - // Thrown when the document has issues being read + // Thrown when the document has issues being read. + + // IndexOutOfBoundsException: + // This gets thrown in certain images. The reason is unknown. It is + // likely due to problems with the file formats that POI is poorly + // handling. return null; } catch (Throwable ex) { @@ -341,7 +365,7 @@ class ImageExtractor { // if no images are extracted from the PPT, return null, else initialize // the output folder for image extraction. String outputFolderPath; - if (listOfAllPictures.length == 0) { + if (listOfAllPictures.isEmpty()) { return null; } else { outputFolderPath = getOutputFolderPath(this.parentFileName); @@ -355,26 +379,26 @@ class ImageExtractor { int i = 0; List listOfExtractedImages = new ArrayList<>(); byte[] data = null; - for (PictureData pictureData : listOfAllPictures) { + for (HSLFPictureData pictureData : listOfAllPictures) { // Get image extension, generate image name, write image to the module // output folder, add it to the listOfExtractedImageAbstractFiles - int type = pictureData.getType(); + PictureType type = pictureData.getType(); String ext; switch (type) { - case Picture.JPEG: + case JPEG: ext = ".jpg"; //NON-NLS break; - case Picture.PNG: + case PNG: ext = ".png"; //NON-NLS break; - case Picture.WMF: + case WMF: ext = ".wmf"; //NON-NLS break; - case Picture.EMF: + case EMF: ext = ".emf"; //NON-NLS break; - case Picture.PICT: + case PICT: ext = ".pict"; //NON-NLS break; default: @@ -406,7 +430,7 @@ class ImageExtractor { try { XMLSlideShow pptx = new XMLSlideShow(new ReadContentInputStream(af)); - listOfAllPictures = pptx.getAllPictures(); + listOfAllPictures = pptx.getPictureData(); } catch (POIXMLException | IOException ex) { // POIXMLException: // Thrown when document fails to load. @@ -468,12 +492,30 @@ class ImageExtractor { try { Workbook xls = new HSSFWorkbook(new ReadContentInputStream(af)); listOfAllPictures = xls.getAllPictures(); - } catch (OldFileFormatException | IOException ex) { - // OldFileFormatException: - // Thrown when the document version is unsupported + } catch (IOException | LeftoverDataException | + RecordFormatException | IllegalArgumentException | + IndexOutOfBoundsException ex) { + // IllegalArgumentException: + // This will catch OldFileFormatException, which is thrown when the + // document version is unsupported. The IllegalArgumentException may + // also get thrown for unknown reasons. // IOException: - // Thrown when the document has issues being read + // Thrown when the document has issues being read. + + // LeftoverDataException: + // This is thrown for poorly formatted files that have more data + // than expected. + + // RecordFormatException: + // This is thrown for poorly formatted files that have less data + // that expected. + + // IllegalArgumentException: + // IndexOutOfBoundsException: + // These get thrown in certain images. The reason is unknown. It is + // likely due to problems with the file formats that POI is poorly + // handling. return null; } catch (Throwable ex) { diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java index 1741f43064..bdf45017d0 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java @@ -169,9 +169,6 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp this.equalitySignComboBox.setSelectedIndex(2); } - /** - * @inheritDoc - */ @Override public void saveSettings() { try { @@ -203,17 +200,11 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp ingestWarningLabel.setVisible(!isEnabled); } - /** - * @inheritDoc - */ @Override public void store() { this.saveSettings(); } - /** - * @inheritDoc - */ @Override public void load() { this.resetComponents(); diff --git a/Core/src/org/sleuthkit/autopsy/framework/Bundle.properties b/Core/src/org/sleuthkit/autopsy/progress/Bundle.properties similarity index 100% rename from Core/src/org/sleuthkit/autopsy/framework/Bundle.properties rename to Core/src/org/sleuthkit/autopsy/progress/Bundle.properties diff --git a/Core/src/org/sleuthkit/autopsy/framework/LoggingProgressIndicator.java b/Core/src/org/sleuthkit/autopsy/progress/LoggingProgressIndicator.java similarity index 92% rename from Core/src/org/sleuthkit/autopsy/framework/LoggingProgressIndicator.java rename to Core/src/org/sleuthkit/autopsy/progress/LoggingProgressIndicator.java index 3fcad59638..6ae7ebdb2d 100644 --- a/Core/src/org/sleuthkit/autopsy/framework/LoggingProgressIndicator.java +++ b/Core/src/org/sleuthkit/autopsy/progress/LoggingProgressIndicator.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.framework; +package org.sleuthkit.autopsy.progress; import java.util.logging.Level; import org.sleuthkit.autopsy.coreutils.Logger; @@ -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/progress/ModalDialogProgressIndicator.java similarity index 56% rename from Core/src/org/sleuthkit/autopsy/framework/ModalDialogProgressIndicator.java rename to Core/src/org/sleuthkit/autopsy/progress/ModalDialogProgressIndicator.java index c5df4c8cfa..d49574bf24 100644 --- a/Core/src/org/sleuthkit/autopsy/framework/ModalDialogProgressIndicator.java +++ b/Core/src/org/sleuthkit/autopsy/progress/ModalDialogProgressIndicator.java @@ -16,11 +16,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.framework; +package org.sleuthkit.autopsy.progress; 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,21 @@ 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 String title; private final ProgressPanel progressPanel; - private final Dialog dialog; + private final Object[] buttonLabels; + private final Object focusedButtonLabel; private final ActionListener buttonListener; + private Dialog dialog; + @GuardedBy("this") + private boolean cancelling; /** * Creates a progress indicator that displays progress using a modal dialog @@ -53,17 +62,11 @@ public final class ModalDialogProgressIndicator implements ProgressIndicator { */ public ModalDialogProgressIndicator(Frame parent, String title, Object[] buttonLabels, Object focusedButtonLabel, ActionListener buttonListener) { this.parent = parent; + this.title = title; progressPanel = new ProgressPanel(); - DialogDescriptor dialogDescriptor = new DialogDescriptor( - progressPanel, - title, - true, - buttonLabels, - focusedButtonLabel, - DialogDescriptor.BOTTOM_ALIGN, - HelpCtx.DEFAULT_HELP, - buttonListener); - dialog = DialogDisplayer.getDefault().createDialog(dialogDescriptor); + progressPanel.setIndeterminate(true); + this.buttonLabels = buttonLabels; + this.focusedButtonLabel = focusedButtonLabel; this.buttonListener = buttonListener; } @@ -76,32 +79,12 @@ public final class ModalDialogProgressIndicator implements ProgressIndicator { */ public ModalDialogProgressIndicator(Frame parent, String title) { this.parent = parent; + this.title = title; progressPanel = new ProgressPanel(); - 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; + progressPanel.setIndeterminate(true); + this.buttonLabels = null; + this.focusedButtonLabel = null; + this.buttonListener = null; } /** @@ -112,14 +95,13 @@ 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); + displayDialog(); }); } @@ -130,13 +112,27 @@ 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); + displayDialog(); + }); + } + + /** + * 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 +143,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 +161,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 +178,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 +194,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,29 +211,52 @@ 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); }); } + /** + * Creates and dislpays the dialog for the progress indicator. + */ + private void displayDialog() { + if (null != buttonLabels && null != focusedButtonLabel && null != buttonListener) { + /* + * Dialog with buttons. + */ + DialogDescriptor dialogDescriptor = new DialogDescriptor( + progressPanel, + title, + true, + buttonLabels, + focusedButtonLabel, + DialogDescriptor.BOTTOM_ALIGN, + HelpCtx.DEFAULT_HELP, + buttonListener); + dialog = DialogDisplayer.getDefault().createDialog(dialogDescriptor); + } else { + /* + * Dialog without buttons. + */ + dialog = new JDialog(parent, title, true); + dialog.add(progressPanel); + dialog.pack(); + } + dialog.setLocationRelativeTo(parent); + this.dialog.setVisible(true); + } } diff --git a/Core/src/org/sleuthkit/autopsy/framework/ProgressIndicator.java b/Core/src/org/sleuthkit/autopsy/progress/ProgressIndicator.java similarity index 95% rename from Core/src/org/sleuthkit/autopsy/framework/ProgressIndicator.java rename to Core/src/org/sleuthkit/autopsy/progress/ProgressIndicator.java index 0f51d98d75..8e5b881182 100644 --- a/Core/src/org/sleuthkit/autopsy/framework/ProgressIndicator.java +++ b/Core/src/org/sleuthkit/autopsy/progress/ProgressIndicator.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.framework; +package org.sleuthkit.autopsy.progress; /** * An interface for progress indicators. A progress indicator can run in @@ -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.form b/Core/src/org/sleuthkit/autopsy/progress/ProgressPanel.form similarity index 91% rename from Core/src/org/sleuthkit/autopsy/framework/ProgressPanel.form rename to Core/src/org/sleuthkit/autopsy/progress/ProgressPanel.form index 59678117af..75597eef34 100644 --- a/Core/src/org/sleuthkit/autopsy/framework/ProgressPanel.form +++ b/Core/src/org/sleuthkit/autopsy/progress/ProgressPanel.form @@ -42,7 +42,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/framework/ProgressPanel.java b/Core/src/org/sleuthkit/autopsy/progress/ProgressPanel.java similarity index 97% rename from Core/src/org/sleuthkit/autopsy/framework/ProgressPanel.java rename to Core/src/org/sleuthkit/autopsy/progress/ProgressPanel.java index 4957a317b6..b918d1f9f0 100644 --- a/Core/src/org/sleuthkit/autopsy/framework/ProgressPanel.java +++ b/Core/src/org/sleuthkit/autopsy/progress/ProgressPanel.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.framework; +package org.sleuthkit.autopsy.progress; /** * A progress panel consisting of a message label and a progress bar. @@ -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/progress/SilentProgressIndicator.java similarity index 90% rename from Core/src/org/sleuthkit/autopsy/framework/SilentProgressIndicator.java rename to Core/src/org/sleuthkit/autopsy/progress/SilentProgressIndicator.java index 8152a097c9..c7ed9c671c 100644 --- a/Core/src/org/sleuthkit/autopsy/framework/SilentProgressIndicator.java +++ b/Core/src/org/sleuthkit/autopsy/progress/SilentProgressIndicator.java @@ -16,9 +16,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.framework; +package org.sleuthkit.autopsy.progress; -import org.sleuthkit.autopsy.framework.ProgressIndicator; +import org.sleuthkit.autopsy.progress.ProgressIndicator; /** * A "silent" or "null" progress indicator. @@ -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/report/ReportExcel.java b/Core/src/org/sleuthkit/autopsy/report/ReportExcel.java index c1cffc9243..56de46f04d 100644 --- a/Core/src/org/sleuthkit/autopsy/report/ReportExcel.java +++ b/Core/src/org/sleuthkit/autopsy/report/ReportExcel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-2014 Basis Technology Corp. + * Copyright 2013-2017 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -80,15 +80,15 @@ class ReportExcel implements TableReportModule { Font titleFont = wb.createFont(); titleFont.setFontHeightInPoints((short) 12); titleStyle.setFont(titleFont); - titleStyle.setAlignment(CellStyle.ALIGN_LEFT); + titleStyle.setAlignment(HorizontalAlignment.LEFT); titleStyle.setWrapText(true); setStyle = wb.createCellStyle(); Font setFont = wb.createFont(); setFont.setFontHeightInPoints((short) 14); - setFont.setBoldweight((short) 10); + setFont.setBold(true); setStyle.setFont(setFont); - setStyle.setAlignment(CellStyle.ALIGN_LEFT); + setStyle.setAlignment(HorizontalAlignment.LEFT); setStyle.setWrapText(true); elementStyle = wb.createCellStyle(); @@ -96,7 +96,7 @@ class ReportExcel implements TableReportModule { Font elementFont = wb.createFont(); elementFont.setFontHeightInPoints((short) 14); elementStyle.setFont(elementFont); - elementStyle.setAlignment(CellStyle.ALIGN_LEFT); + elementStyle.setAlignment(HorizontalAlignment.LEFT); elementStyle.setWrapText(true); writeSummaryWorksheet(); diff --git a/Core/src/org/sleuthkit/autopsy/test/InterestingArtifactCreatorIngestModule.java b/Core/src/org/sleuthkit/autopsy/test/InterestingArtifactCreatorIngestModule.java new file mode 100644 index 0000000000..64d374d636 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/test/InterestingArtifactCreatorIngestModule.java @@ -0,0 +1,126 @@ +/* + * 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.test; + +import java.util.logging.Level; + +import org.openide.util.Exceptions; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.services.Blackboard; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.ingest.FileIngestModuleAdapter; +import org.sleuthkit.autopsy.ingest.IngestJobContext; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * A file ingest module that creates some interestng artifacts + * with attributes based on files for test purposes. + */ + +final class InterestingArtifactCreatorIngestModule extends FileIngestModuleAdapter { + + private static final Logger logger = Logger.getLogger(InterestingArtifactCreatorIngestModule.class.getName()); + private static final String MODULE_NAME = InterestingArtifactCreatorIngestModuleFactory.getModuleName(); + private static final String[] ARTIFACT_TYPE_NAMES = {"TSK_WEB_BOOKMARK", "TSK_KEYWORD_HIT", "TSK_CALLLOG"}; + private static final String[] ARTIFACT_DISPLAY_NAMES = {"Web Bookmarks", "Keyword Hits", "Call Logs"}; + private static final String INT_ARTIFACT_TYPE_NAME = BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_ARTIFACT_HIT.getLabel(); + private static final String INT_ARTIFACT_DISPLAY_NAME = BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_ARTIFACT_HIT.getDisplayName(); + private BlackboardArtifact.Type artifactType; + + @Override + public void startUp(IngestJobContext context) throws IngestModuleException { + Blackboard blackboard = Case.getCurrentCase().getServices().getBlackboard(); + try { + artifactType = blackboard.getOrAddArtifactType(INT_ARTIFACT_TYPE_NAME, INT_ARTIFACT_DISPLAY_NAME); + } catch (Blackboard.BlackboardException ex) { + throw new IngestModuleException(Bundle.ErrorCreatingCustomBlackBoardType(), ex); + } + } + + @Override + public ProcessResult process(AbstractFile file) { + /* + * Skip directories and virtual files. + */ + if (file.isDir() || file.isVirtual()) { + return ProcessResult.OK; + } + + try { + /* + * Add a custom artifact with one custom attribute of each value + * type. + */ + int randomArtIndex = (int) (Math.random() * 3); + Blackboard blackboard = Case.getCurrentCase().getServices().getBlackboard(); + BlackboardArtifact.Type artifactTypeBase = blackboard.getOrAddArtifactType(ARTIFACT_TYPE_NAMES[randomArtIndex], ARTIFACT_DISPLAY_NAMES[randomArtIndex]); + BlackboardArtifact artifactBase = file.newArtifact(artifactTypeBase.getTypeID()); + String commentTxt; + BlackboardAttribute baseAttr; + switch (artifactBase.getArtifactTypeID()) { + case 2: + commentTxt = "www.placeholderWebsiteDOTCOM"; + baseAttr = new BlackboardAttribute( + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL, "Fake Web BookMark", "www.thisWebsiteIsStillFake.com"); + artifactBase.addAttribute(baseAttr); + break; + case 9: + commentTxt = "fakeKeyword"; + baseAttr = new BlackboardAttribute( + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD_PREVIEW, "Fake Keyword Search", "Fake Keyword Preview Text"); + BlackboardAttribute set = new BlackboardAttribute( + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME, "Fake Keyword Search", "Fake"); + BlackboardAttribute keyword = new BlackboardAttribute( + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD, "Fake Keyword Search", "FakeKeyword"); + artifactBase.addAttribute(baseAttr); + artifactBase.addAttribute(set); + artifactBase.addAttribute(keyword); + break; + case 25: + commentTxt = "fake phone number from"; + baseAttr = new BlackboardAttribute( + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_FROM, "Fake Call Log Whatever", "555-555-5555"); + artifactBase.addAttribute(baseAttr); + break; + default: + commentTxt = "DEPENDENT ON ARTIFACT TYPE"; + break; + } + BlackboardArtifact artifact = file.newArtifact(artifactType.getTypeID()); + BlackboardAttribute att = new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME, MODULE_NAME, "ArtifactsAndTxt"); + + BlackboardAttribute att2 = new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COMMENT, MODULE_NAME, commentTxt); + BlackboardAttribute att3 = new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_CATEGORY, MODULE_NAME, ""); + artifact.addAttribute(att); + artifact.addAttribute(att2); + artifact.addAttribute(att3); + artifact.addAttribute(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT, MODULE_NAME, artifactBase.getArtifactID())); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, String.format("Failed to process file (obj_id = %d)", file.getId()), ex); + return ProcessResult.ERROR; + } catch (Blackboard.BlackboardException ex) { + Exceptions.printStackTrace(ex); + } + return ProcessResult.OK; + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/test/InterestingArtifactCreatorIngestModuleFactory.java b/Core/src/org/sleuthkit/autopsy/test/InterestingArtifactCreatorIngestModuleFactory.java new file mode 100644 index 0000000000..7112f2147e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/test/InterestingArtifactCreatorIngestModuleFactory.java @@ -0,0 +1,64 @@ +/* + * 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.test; + +import org.openide.util.lookup.ServiceProvider; +import org.sleuthkit.autopsy.coreutils.Version; +import org.sleuthkit.autopsy.ingest.FileIngestModule; +import org.sleuthkit.autopsy.ingest.IngestModuleFactory; +import org.sleuthkit.autopsy.ingest.IngestModuleFactoryAdapter; +import org.sleuthkit.autopsy.ingest.IngestModuleIngestJobSettings; + +/** + * A factory for file ingest modules that creates some interestng artifacts with + * attributes based on files for test purposes. Uncomment the service provider + * annotation to activate this test fixture. + */ +//@ServiceProvider(service = IngestModuleFactory.class) +public final class InterestingArtifactCreatorIngestModuleFactory extends IngestModuleFactoryAdapter { + + @Override + public String getModuleDisplayName() { + return getModuleName(); + } + + @Override + public String getModuleDescription() { + return "Creates some interestng artifacts with attributes based on files for test purposes."; + } + + @Override + public String getModuleVersionNumber() { + return Version.getVersion(); + } + + @Override + public boolean isFileIngestModuleFactory() { + return true; + } + + @Override + public FileIngestModule createFileIngestModule(IngestModuleIngestJobSettings settings) { + return new InterestingArtifactCreatorIngestModule(); + } + + static String getModuleName() { + return "Test Interesting Artifact Creator"; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/test/TestAutopsyService.java b/Core/src/org/sleuthkit/autopsy/test/TestAutopsyService.java index b550ca647d..4fb2997691 100644 --- a/Core/src/org/sleuthkit/autopsy/test/TestAutopsyService.java +++ b/Core/src/org/sleuthkit/autopsy/test/TestAutopsyService.java @@ -21,8 +21,8 @@ package org.sleuthkit.autopsy.test; import java.util.logging.Level; import org.openide.util.lookup.ServiceProvider; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.framework.AutopsyService; -import org.sleuthkit.autopsy.framework.ProgressIndicator; +import org.sleuthkit.autopsy.appservices.AutopsyService; +import org.sleuthkit.autopsy.progress.ProgressIndicator; /** * An implementation of the Autopsy service interface used for test purposes. @@ -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/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventRootNode.java b/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventRootNode.java index a2a00837c6..1b35d90188 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventRootNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventRootNode.java @@ -100,11 +100,15 @@ public class EventRootNode extends DisplayableItemNode { */ if (eventIDs.size() < MAX_EVENTS_TO_DISPLAY) { for (Long eventId: eventIDs){ - nodesMap.put(eventId, createNode(eventId)); - toPopulate.add(eventId); + if (!nodesMap.containsKey(eventId)) { + nodesMap.put(eventId, createNode(eventId)); + } + toPopulate.add(eventId); } } else { - nodesMap.put(-1L, createNode(-1L)); + if (!nodesMap.containsKey(-1L)) { + nodesMap.put(-1L, createNode(-1L)); + } toPopulate.add(-1L); } return true; diff --git a/CoreLibs/ivy.xml b/CoreLibs/ivy.xml index c0ca478735..8043007b52 100644 --- a/CoreLibs/ivy.xml +++ b/CoreLibs/ivy.xml @@ -28,8 +28,8 @@ - - + + diff --git a/CoreLibs/nbproject/project.properties b/CoreLibs/nbproject/project.properties index 3f1452877a..62c4d535e6 100644 --- a/CoreLibs/nbproject/project.properties +++ b/CoreLibs/nbproject/project.properties @@ -6,7 +6,8 @@ file.reference.avalon-framework-4.1.5.jar=release/modules/ext/avalon-framework-4 file.reference.common-image-3.2.jar=release/modules/ext/common-image-3.2.jar file.reference.common-io-3.2.jar=release/modules/ext/common-io-3.2.jar file.reference.common-lang-3.2.jar=release/modules/ext/common-lang-3.2.jar -file.reference.commons-codec-1.5.jar=release/modules/ext/commons-codec-1.5.jar +file.reference.commons-codec-1.10.jar=release/modules/ext/commons-codec-1.10.jar +file.reference.commons-collections4-4.1.jar=release/modules/ext/commons-collections4-4.1.jar file.reference.commons-csv-1.4.jar=release/modules/ext/commons-csv-1.4.jar file.reference.commons-io-2.4.jar=release/modules/ext/commons-io-2.4.jar file.reference.commons-lang-2.6.jar=release/modules/ext/commons-lang-2.6.jar @@ -56,11 +57,11 @@ file.reference.logkit-1.0.1.jar=release/modules/ext/logkit-1.0.1.jar file.reference.mail-1.4.3.jar=release/modules/ext/mail-1.4.3.jar file.reference.openjfx-dialogs-1.0.2.jar=release/modules/ext/openjfx-dialogs-1.0.3.jar file.reference.platform-3.4.0.jar=release/modules/ext/platform-3.4.0.jar -file.reference.poi-3.8.jar=release/modules/ext/poi-3.8.jar -file.reference.poi-excelant-3.8.jar=release/modules/ext/poi-excelant-3.8.jar -file.reference.poi-ooxml-3.8.jar=release/modules/ext/poi-ooxml-3.8.jar -file.reference.poi-ooxml-schemas-3.8.jar=release/modules/ext/poi-ooxml-schemas-3.8.jar -file.reference.poi-scratchpad-3.8.jar=release/modules/ext/poi-scratchpad-3.8.jar +file.reference.poi-3.15.jar=release/modules/ext/poi-3.15.jar +file.reference.poi-excelant-3.15.jar=release/modules/ext/poi-excelant-3.15.jar +file.reference.poi-ooxml-3.15.jar=release/modules/ext/poi-ooxml-3.15.jar +file.reference.poi-ooxml-schemas-3.15.jar=release/modules/ext/poi-ooxml-schemas-3.15.jar +file.reference.poi-scratchpad-3.15.jar=release/modules/ext/poi-scratchpad-3.15.jar file.reference.reflections-0.9.8.jar=release/modules/ext/reflections-0.9.8.jar file.reference.servlet-api-2.5.jar=release/modules/ext/servlet-api-2.5.jar file.reference.sigar-1.6.4-sources.jar=release/modules/ext/sigar-1.6.4-sources.jar @@ -69,7 +70,7 @@ file.reference.slf4j-api-1.6.1.jar=release/modules/ext/slf4j-api-1.6.1.jar file.reference.slf4j-simple-1.6.1.jar=release/modules/ext/slf4j-simple-1.6.1.jar file.reference.stax-api-1.0.1.jar=release/modules/ext/stax-api-1.0.1.jar file.reference.xml-apis-1.0.b2.jar=release/modules/ext/xml-apis-1.0.b2.jar -file.reference.xmlbeans-2.3.0.jar=release/modules/ext/xmlbeans-2.3.0.jar +file.reference.xmlbeans-2.6.0.jar=release/modules/ext/xmlbeans-2.6.0.jar javac.source=1.8 javac.compilerargs=-Xlint -Xlint:-serial javadoc.reference.commons-csv-1.4.jar=release/modules/ext/commons-csv-1.4-javadoc.jar diff --git a/CoreLibs/nbproject/project.xml b/CoreLibs/nbproject/project.xml index 358637d979..23a41edaf1 100644 --- a/CoreLibs/nbproject/project.xml +++ b/CoreLibs/nbproject/project.xml @@ -700,16 +700,16 @@ release/modules/ext/sigar-1.6.4.jar
- ext/xmlbeans-2.3.0.jar - release/modules/ext/xmlbeans-2.3.0.jar + ext/xmlbeans-2.6.0.jar + release/modules/ext/xmlbeans-2.6.0.jar ext/jna-3.4.0.jar release/modules/ext/jna-3.4.0.jar - ext/poi-ooxml-schemas-3.8.jar - release/modules/ext/poi-ooxml-schemas-3.8.jar + ext/poi-ooxml-schemas-3.15.jar + release/modules/ext/poi-ooxml-schemas-3.15.jar ext/gson-1.4.jar @@ -768,8 +768,8 @@ release/modules/ext/jcalendarbutton-1.4.6.jar - ext/poi-ooxml-3.8.jar - release/modules/ext/poi-ooxml-3.8.jar + ext/poi-ooxml-3.15.jar + release/modules/ext/poi-ooxml-3.15.jar ext/imageio-psd-3.2.jar @@ -779,13 +779,17 @@ ext/stax-api-1.0.1.jar release/modules/ext/stax-api-1.0.1.jar + + ext/commons-collections4-4.1.jar + release/modules/ext/commons-collections4-4.1.jar + ext/servlet-api-2.5.jar release/modules/ext/servlet-api-2.5.jar - ext/poi-excelant-3.8.jar - release/modules/ext/poi-excelant-3.8.jar + ext/poi-excelant-3.15.jar + release/modules/ext/poi-excelant-3.15.jar ext/imageio-pcx-3.2.jar @@ -824,8 +828,8 @@ release/modules/ext/geronimo-jms_1.1_spec-1.0.jar - ext/poi-scratchpad-3.8.jar - release/modules/ext/poi-scratchpad-3.8.jar + ext/poi-scratchpad-3.15.jar + release/modules/ext/poi-scratchpad-3.15.jar ext/joda-time-2.4-sources.jar @@ -876,8 +880,8 @@ release/modules/ext/ant-1.8.2.jar - ext/commons-codec-1.5.jar - release/modules/ext/commons-codec-1.5.jar + ext/commons-codec-1.10.jar + release/modules/ext/commons-codec-1.10.jar ext/javassist-3.12.1.GA.jar @@ -896,8 +900,8 @@ release/modules/ext/commons-io-2.4.jar - ext/poi-3.8.jar - release/modules/ext/poi-3.8.jar + ext/poi-3.15.jar + release/modules/ext/poi-3.15.jar ext/controlsfx-8.40.11.jar diff --git a/Experimental/ivy.xml b/Experimental/ivy.xml index 5934399ba3..658e7e5948 100644 --- a/Experimental/ivy.xml +++ b/Experimental/ivy.xml @@ -7,7 +7,7 @@ - + diff --git a/Experimental/nbproject/project.properties b/Experimental/nbproject/project.properties index 34d831c12a..d47542bdb5 100644 --- a/Experimental/nbproject/project.properties +++ b/Experimental/nbproject/project.properties @@ -3,7 +3,7 @@ file.reference.jackson-core-2.7.0.jar=release/modules/ext/jackson-core-2.7.0.jar file.reference.LGoodDatePicker-4.3.1.jar=release/modules/ext/LGoodDatePicker-4.3.1.jar file.reference.mchange-commons-java-0.2.9.jar=release/modules/ext/mchange-commons-java-0.2.9.jar file.reference.postgresql-9.4-1201-jdbc41.jar=release/modules/ext/postgresql-9.4-1201-jdbc41.jar -file.reference.tika-core-1.5.jar=release/modules/ext/tika-core-1.5.jar +file.reference.tika-core-1.14.jar=release/modules/ext/tika-core-1.14.jar javac.source=1.8 javac.compilerargs=-Xlint -Xlint:-serial javadoc.reference.LGoodDatePicker-4.3.1.jar=release/modules/ext/LGoodDatePicker-4.3.1-javadoc.jar diff --git a/Experimental/nbproject/project.xml b/Experimental/nbproject/project.xml index 714d6e4028..f0e1b77527 100644 --- a/Experimental/nbproject/project.xml +++ b/Experimental/nbproject/project.xml @@ -132,8 +132,8 @@ release/modules/ext/LGoodDatePicker-4.3.1.jar - ext/tika-core-1.5.jar - release/modules/ext/tika-core-1.5.jar + ext/tika-core-1.14.jar + release/modules/ext/tika-core-1.14.jar ext/jackson-core-2.7.0.jar diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestCasePanel.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestCasePanel.java index 20d2a989cb..ea98faea18 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestCasePanel.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestCasePanel.java @@ -39,10 +39,11 @@ import javax.swing.SwingWorker; import javax.swing.event.ListSelectionEvent; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableColumn; -import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.casemodule.CaseActionCancelledException; import org.sleuthkit.autopsy.casemodule.CaseMetadata; import org.sleuthkit.autopsy.casemodule.StartupWindowProvider; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; /** * A panel that allows a user to open cases created by auto ingest. @@ -295,18 +296,12 @@ public final class AutoIngestCasePanel extends JPanel { protected void done() { try { get(); - } catch (InterruptedException ex) { - logger.log(Level.SEVERE, String.format("Error while opening case with case metadata file path %s", caseMetadataFilePath), ex); - JOptionPane.showMessageDialog(WindowManager.getDefault().getMainWindow(), - ex.getMessage(), - org.openide.util.NbBundle.getMessage(AutoIngestCasePanel.class, "ReviewModeCasePanel.cannotOpenCase"), - JOptionPane.ERROR_MESSAGE); - } catch (ExecutionException ex) { - logger.log(Level.SEVERE, String.format("Error while opening case with case metadata file path %s", caseMetadataFilePath), ex); - JOptionPane.showMessageDialog(WindowManager.getDefault().getMainWindow(), - ex.getCause().getMessage(), - org.openide.util.NbBundle.getMessage(AutoIngestCasePanel.class, "ReviewModeCasePanel.cannotOpenCase"), - JOptionPane.ERROR_MESSAGE); + } catch (InterruptedException | ExecutionException ex) { + 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 + MessageNotifyUtil.Message.error(ex.getCause().getLocalizedMessage()); + } + StartupWindowProvider.getInstance().open(); } finally { setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); } diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java index 900a5a603b..d92e8d2dbe 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java @@ -92,8 +92,8 @@ import static org.sleuthkit.autopsy.experimental.autoingest.ManifestNodeData.Pro import org.sleuthkit.autopsy.experimental.configuration.AutoIngestUserPreferences; import org.sleuthkit.autopsy.experimental.configuration.SharedConfiguration; import org.sleuthkit.autopsy.experimental.configuration.SharedConfiguration.SharedConfigurationException; -import org.sleuthkit.autopsy.framework.AutoIngestDataSourceProcessor; -import org.sleuthkit.autopsy.framework.AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException; +import org.sleuthkit.autopsy.datasourceprocessors.AutoIngestDataSourceProcessor; +import org.sleuthkit.autopsy.datasourceprocessors.AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException; import org.sleuthkit.autopsy.ingest.IngestJob; import org.sleuthkit.autopsy.ingest.IngestJob.CancellationReason; import org.sleuthkit.autopsy.ingest.IngestJobSettings; diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutopsyManifestFileParser.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutopsyManifestFileParser.java index 0fe33092ca..f25182b018 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutopsyManifestFileParser.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutopsyManifestFileParser.java @@ -39,10 +39,10 @@ import org.xml.sax.SAXException; public final class AutopsyManifestFileParser implements ManifestFileParser { private static final String MANIFEST_FILE_NAME_SIGNATURE = "_Manifest.xml"; - private static final String ROOT_ELEM_TAG_NAME = "Manifest"; - private static final String CASE_NAME_XPATH = "/Manifest/Collection/Name/text()"; - private static final String DEVICE_ID_XPATH = "/Manifest/Collection/Image/ID/text()"; - private static final String DATA_SOURCE_NAME_XPATH = "/Manifest/Collection/Image/Name/text()"; + private static final String ROOT_ELEM_TAG_NAME = "AutopsyManifest"; + private static final String CASE_NAME_XPATH = "/AutopsyManifest/CaseName/text()"; + private static final String DEVICE_ID_XPATH = "/AutopsyManifest/DeviceId/text()"; + private static final String DATA_SOURCE_NAME_XPATH = "/AutopsyManifest/DataSource/text()"; @Override public boolean fileIsManifest(Path filePath) { @@ -62,10 +62,6 @@ public final class AutopsyManifestFileParser implements ManifestFileParser { @Override public Manifest parse(Path filePath) throws ManifestFileParserException { - if (!fileIsManifest(filePath)) { - throw new ManifestFileParserException(String.format("%s not recognized as a manifest", filePath)); - } - try { Document doc = this.createManifestDOM(filePath); XPath xpath = XPathFactory.newInstance().newXPath(); diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ManifestFileParser.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ManifestFileParser.java index 00fb2b9a5f..863155afcc 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ManifestFileParser.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ManifestFileParser.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2015 Basis Technology Corp. + * Copyright 2015-2017 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,9 +20,28 @@ package org.sleuthkit.autopsy.experimental.autoingest; import java.nio.file.Path; +/** + * Responsible for parsing the manifest files that + * describe cases, devices, and data sources. + * These are used by autoingest to create cases and add + * data sources to the correct case. + */ public interface ManifestFileParser { + /** + * Checks if a file is this type of manifest file + * @param filePath Path to potential manifest file + * @return True if the file is a manifest that this parser supports + */ boolean fileIsManifest(Path filePath); + + /** + * Parses the given file. Will only be called if + * fileIsManifest() previously returned true. + * @param filePath Path to manifest file + * @return Parsed results + * @throws org.sleuthkit.autopsy.experimental.autoingest.ManifestFileParser.ManifestFileParserException + */ Manifest parse(Path filePath) throws ManifestFileParserException; public final static class ManifestFileParserException extends Exception { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java index 661a895db9..71ff3681a5 100755 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenAction.java @@ -35,6 +35,7 @@ import org.openide.util.actions.Presenter; import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.core.Installer; +import org.sleuthkit.autopsy.core.RuntimeProperties; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.ImageGalleryModule; @@ -64,7 +65,7 @@ public final class OpenAction extends CallableSystemAction implements Presenter. toolbarButton.addActionListener(actionEvent -> performAction()); pcl = (PropertyChangeEvent evt) -> { if (evt.getPropertyName().equals(Case.Events.CURRENT_CASE.toString())) { - setEnabled(evt.getNewValue() != null); + setEnabled(RuntimeProperties.runningWithGUI() && evt.getNewValue() != null); } }; Case.addPropertyChangeListener(pcl); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupSortBy.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupSortBy.java index 8239837d6a..20d47e2952 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupSortBy.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupSortBy.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-16 Basis Technology Corp. + * Copyright 2011-17 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -54,7 +54,7 @@ public class GroupSortBy implements Comparator { /** * sort the groups by some priority metric to be determined and implemented */ - public final static GroupSortBy PRIORITY = new GroupSortBy(Bundle.GroupSortBy_priority(), "hashset_hits.png", Comparator.comparing(DrawableGroup::getHashHitDensity).thenComparing(Comparator.comparing(DrawableGroup::getUncategorizedCount))); + public final static GroupSortBy PRIORITY = new GroupSortBy(Bundle.GroupSortBy_priority(), "hashset_hits.png", Comparator.comparing(DrawableGroup::getHashHitDensity).thenComparing(Comparator.comparing(DrawableGroup::getUncategorizedCount)).reversed()); @Override public int compare(DrawableGroup o1, DrawableGroup o2) { diff --git a/KeywordSearch/ivy.xml b/KeywordSearch/ivy.xml index 6135777192..3ceb4e6378 100644 --- a/KeywordSearch/ivy.xml +++ b/KeywordSearch/ivy.xml @@ -5,10 +5,6 @@ - - - - @@ -22,19 +18,6 @@ - - - - - - - - - - - - - diff --git a/KeywordSearch/nbproject/project.properties b/KeywordSearch/nbproject/project.properties index 3a5a642995..5aa1b716e0 100644 --- a/KeywordSearch/nbproject/project.properties +++ b/KeywordSearch/nbproject/project.properties @@ -124,9 +124,6 @@ file.reference.slf4j-api-1.7.12.jar=release/modules/ext/slf4j-api-1.7.12.jar file.reference.solr-solrj-4.9.1-javadoc.jar=release/modules/ext/solr-solrj-4.9.1-javadoc.jar file.reference.solr-solrj-4.9.1-sources.jar=release/modules/ext/solr-solrj-4.9.1-sources.jar file.reference.solr-solrj-4.9.1.jar=release/modules/ext/solr-solrj-4.9.1.jar -file.reference.solr-solrj-6.2.1-javadoc.jar=release/modules/ext/solr-solrj-6.2.1-javadoc.jar -file.reference.solr-solrj-6.2.1-sources.jar=release/modules/ext/solr-solrj-6.2.1-sources.jar -file.reference.solr-solrj-6.2.1.jar=release/modules/ext/solr-solrj-6.2.1.jar file.reference.spring-aop-3.1.2.RELEASE.jar=release/modules/ext/spring-aop-3.1.2.RELEASE.jar file.reference.spring-asm-3.1.2.RELEASE.jar=release/modules/ext/spring-asm-3.1.2.RELEASE.jar file.reference.spring-beans-3.1.2.RELEASE.jar=release/modules/ext/spring-beans-3.1.2.RELEASE.jar diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties index 050e2d687e..99d88938ce 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties @@ -213,7 +213,7 @@ Server.queryNumFileChunks.exception.msg=Error getting number of file chunks, Server.query.exception.msg=Error running query\: {0} Server.query2.exception.msg=Error running query\: {0} Server.queryTerms.exception.msg=Error running terms query\: {0} -Server.connect.exception.msg=Failed to connect to Solr server\: +Server.connect.exception.msg=Failed to connect to Solr server\: {0} Server.openCore.exception.msg=Keyword search service not yet running Server.openCore.exception.cantOpen.msg=Could not create or open index Server.openCore.exception.noIndexDir.msg=Index directory could not be created or is missing @@ -315,8 +315,6 @@ GlobalListsManagementPanel.copyListButton.text=Copy List GlobalListsManagementPanel.renameListButton.text=Edit List Name GlobalEditListPanel.editWordButton.text=Edit Keyword SolrSearchService.ServiceName=Solr Keyword Search Service -SolrSearchService.IndexUpgradeDialog.title=Text Index Upgrade Required In Order To Open Case -SolrSearchService.IndexUpgradeDialog.msg=The text index upgrade can take some time.
When completed, you will be able to see existing keyword search results and perform literal keyword searches,
but you will not be able to add new text to the index or perform regex searches. You may instead open the case
with your previous version of this application. Do you wish to proceed with the index upgrade? SolrSearchService.IndexReadOnlyDialog.title=Text Index Is Read-Only -SolrSearchService.IndexReadOnlyDialog.msg=The text index for this case is read-only.
You will be able to see existing keyword search results and perform literal keyword searches,
but you will not be able to add new text to the index or perform regex searches. You may instead open the case
with your previous version of this application. +SolrSearchService.IndexReadOnlyDialog.msg=The text index for this case is read-only.
You will be able to see existing keyword search results and perform exact match and substring match keyword searches,
but you will not be able to add new text to the index or perform regex searches. You may instead open the case
with your previous version of this application. diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle_ja.properties b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle_ja.properties index 78412ea29e..04b64d7414 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle_ja.properties +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle_ja.properties @@ -270,7 +270,7 @@ HighlightedMatchesSource.getMarkup.queryFailedMsg=
>> responseHighlight = response.getHighlighting();
-            String highlightField = isLiteral
-                    ? LuceneQuery.HIGHLIGHT_FIELD
-                    : Server.Schema.CONTENT_STR.toString();
+
             if (responseHighlight == null) {
                 highlightedContent = attemptManualHighlighting(response.getResults(), highlightField, keywords);
             } else {
@@ -433,6 +445,7 @@ class HighlightedText implements IndexedText {
             return 0;
         }
         return this.numberOfHitsPerPage.get(this.currentPage);
+
     }
 
     /**
@@ -445,7 +458,7 @@ class HighlightedText implements IndexedText {
      *                         to a Solr query. We expect there to only ever be
      *                         a single document.
      *
-     * @return Either a string with the keyword highlighted or a string
+     * @return Either a string with the keyword highlighted via HTML span tags or a string
      *         indicating that we did not find a hit in the document.
      */
     static String attemptManualHighlighting(SolrDocumentList solrDocumentList, String highlightField, Collection keywords) {
@@ -465,38 +478,41 @@ class HighlightedText implements IndexedText {
         // not see highlighted text in the content viewer.
         text = StringEscapeUtils.escapeHtml(text);
 
-        StringBuilder highlightedText = new StringBuilder("");
+        TreeRangeSet highlights = TreeRangeSet.create();
 
-        //do a highlighting pass for each keyword
+        //for each keyword find the locations of hits and record them in the RangeSet
         for (String keyword : keywords) {
-            //we also need to escape the keyword so that it matches the escpared text
+            //we also need to escape the keyword so that it matches the escaped text
             final String escapedKeyword = StringEscapeUtils.escapeHtml(keyword);
-            int textOffset = 0;
-            int hitOffset = StringUtils.indexOfIgnoreCase(text, escapedKeyword, textOffset);
+            int searchOffset = 0;
+            int hitOffset = StringUtils.indexOfIgnoreCase(text, escapedKeyword, searchOffset);
             while (hitOffset != -1) {
-                // Append the portion of text up to (but not including) the hit.
-                highlightedText.append(text.substring(textOffset, hitOffset));
-                // Add in the highlighting around the keyword.
-                highlightedText.append(HIGHLIGHT_PRE);
-                highlightedText.append(keyword);
-                highlightedText.append(HIGHLIGHT_POST);
+                // Advance the search offset past the keyword.
+                searchOffset = hitOffset + escapedKeyword.length();
 
-                // Advance the text offset past the keyword.
-                textOffset = hitOffset + escapedKeyword.length();
+                //record the location of the hit, possibly merging it with other hits
+                highlights.add(Range.closedOpen(hitOffset, searchOffset));
 
-                hitOffset = StringUtils.indexOfIgnoreCase(text, escapedKeyword, textOffset);
+                //look for next hit
+                hitOffset = StringUtils.indexOfIgnoreCase(text, escapedKeyword, searchOffset);
             }
-            // Append the remainder of text field
-            highlightedText.append(text.substring(textOffset, text.length()));
-
-            if (highlightedText.length() == 0) {
-                return NbBundle.getMessage(HighlightedText.class, "HighlightedMatchesSource.getMarkup.noMatchMsg");
-            }
-            //reset for next pass
-            text = highlightedText.toString();
-            highlightedText = new StringBuilder("");
         }
-        return text;
+
+        StringBuilder highlightedText = new StringBuilder(text);
+        int totalHighLightLengthInserted = 0;
+        //for each range to be highlighted...
+        for (Range highlightRange : highlights.asRanges()) {
+            int hStart = highlightRange.lowerEndpoint();
+            int hEnd = highlightRange.upperEndpoint();
+
+            //insert the pre and post tag, adjusting indices for previously added tags
+            highlightedText.insert(hStart + totalHighLightLengthInserted, HIGHLIGHT_PRE);
+            totalHighLightLengthInserted += HIGHLIGHT_PRE.length();
+            highlightedText.insert(hEnd + totalHighLightLengthInserted, HIGHLIGHT_POST);
+            totalHighLightLengthInserted += HIGHLIGHT_POST.length();
+        }
+
+        return highlightedText.toString();
     }
 
     /**
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/HtmlTextExtractor.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/HtmlTextExtractor.java
index 5d79639fc5..e5db5b6a10 100644
--- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/HtmlTextExtractor.java
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/HtmlTextExtractor.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,6 +25,8 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.logging.Level;
 import net.htmlparser.jericho.Attributes;
+import net.htmlparser.jericho.Config;
+import net.htmlparser.jericho.LoggerProvider;
 import net.htmlparser.jericho.Renderer;
 import net.htmlparser.jericho.Source;
 import net.htmlparser.jericho.StartTag;
@@ -49,6 +51,11 @@ class HtmlTextExtractor extends FileTextExtractor {
             "text/html", //NON-NLS NON-NLS
             "text/javascript" //NON-NLS
     );
+    
+    static {
+        // Disable Jericho HTML Parser log messages.
+        Config.LoggerProvider = LoggerProvider.DISABLED;
+    }
 
     @Override
     boolean isContentTypeSpecific() {
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/IndexFinder.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/IndexFinder.java
index 6ecf83bdce..b4a09e226e 100644
--- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/IndexFinder.java
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/IndexFinder.java
@@ -27,7 +27,7 @@ import java.util.List;
 import org.apache.commons.lang.math.NumberUtils;
 import org.sleuthkit.autopsy.casemodule.Case;
 import org.sleuthkit.autopsy.coreutils.Logger;
-import org.sleuthkit.autopsy.framework.AutopsyService;
+import org.sleuthkit.autopsy.appservices.AutopsyService;
 
 /**
  * This class handles the task of finding and identifying KWS index folders.
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java
index ad9e151e9e..0ec43d4b3e 100644
--- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java
@@ -315,16 +315,16 @@ class Ingester {
          *
          * @return The field map of fields that are common to all file classes.
          */
-        private Map getCommonFields(AbstractFile af) {
+        private Map getCommonFields(AbstractFile file) {
             Map params = new HashMap<>();
-            params.put(Server.Schema.ID.toString(), Long.toString(af.getId()));
+            params.put(Server.Schema.ID.toString(), Long.toString(file.getId()));
             try {
-                params.put(Server.Schema.IMAGE_ID.toString(), Long.toString(af.getDataSource().getId()));
+                params.put(Server.Schema.IMAGE_ID.toString(), Long.toString(file.getDataSource().getId()));
             } catch (TskCoreException ex) {
-                logger.log(Level.SEVERE, "Could not get data source id to properly index the file " + af.getId(), ex); //NON-NLS
+                logger.log(Level.SEVERE, "Could not get data source id to properly index the file " + file.getId(), ex); //NON-NLS
                 params.put(Server.Schema.IMAGE_ID.toString(), Long.toString(-1));
             }
-            params.put(Server.Schema.FILE_NAME.toString(), af.getName());
+            params.put(Server.Schema.FILE_NAME.toString(), file.getName());
             return params;
         }
 
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchIngestModule.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchIngestModule.java
index 6f3c11f8cb..08a6b38165 100644
--- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchIngestModule.java
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchIngestModule.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");
@@ -446,7 +446,7 @@ public final class KeywordSearchIngestModule implements FileIngestModule {
             }
 
             if (extractor == null) {
-                logger.log(Level.INFO, "No text extractor found for file id:{0}, name: {1}, detected format: {2}", new Object[]{aFile.getId(), aFile.getName(), detectedFormat}); //NON-NLS
+                // No text extractor found.
                 return false;
             }
 
@@ -561,7 +561,7 @@ public final class KeywordSearchIngestModule implements FileIngestModule {
                     return;
                 }
                 if (!extractTextAndIndex(aFile, fileType)) {
-                    logger.log(Level.WARNING, "Text extractor not found for file. Extracting strings only. File: ''{0}'' (id:{1}).", new Object[]{aFile.getName(), aFile.getId()}); //NON-NLS
+                    // Text extractor not found for file. Extract string only.
                     putIngestStatus(jobId, aFile.getId(), IngestStatus.SKIPPED_ERROR_TEXTEXTRACT);
                 } else {
                     putIngestStatus(jobId, aFile.getId(), IngestStatus.TEXT_INGESTED);
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java
index a5925e9f2e..3a55aad646 100644
--- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java
@@ -187,7 +187,7 @@ public class Server {
     private int currentSolrServerPort = 0;
     private int currentSolrStopPort = 0;
     private static final boolean DEBUG = false;//(Version.getBuildType() == Version.Type.DEVELOPMENT);
-   private static final String SOLR = "solr";
+    private static final String SOLR = "solr";
     private static final String CORE_PROPERTIES = "core.properties";
 
     public enum CORE_EVT_STATES {
@@ -636,7 +636,7 @@ public class Server {
      * Creates/opens a Solr core (index) for a case.
      *
      * @param theCase The case for which the core is to be created/opened.
-     * @param index The text index that the Solr core should be using.
+     * @param index   The text index that the Solr core should be using.
      *
      * @throws KeywordSearchModuleException If an error occurs while
      *                                      creating/opening the core.
@@ -645,14 +645,14 @@ public class Server {
         currentCoreLock.writeLock().lock();
         try {
             currentCore = openCore(theCase, index);
-            
+
             try {
                 // execute a test query. if it fails, an exception will be thrown
                 queryNumIndexedFiles();
             } catch (NoOpenCoreException ex) {
                 throw new KeywordSearchModuleException(NbBundle.getMessage(this.getClass(), "Server.openCore.exception.cantOpen.msg"), ex);
             }
-            
+
             serverAction.putValue(CORE_EVT, CORE_EVT_STATES.STARTED);
         } finally {
             currentCoreLock.writeLock().unlock();
@@ -684,7 +684,7 @@ public class Server {
             currentCoreLock.readLock().unlock();
         }
     }
-    
+
     void closeCore() throws KeywordSearchModuleException {
         currentCoreLock.writeLock().lock();
         try {
@@ -713,7 +713,6 @@ public class Server {
     /**
      * ** end single-case specific methods ***
      */
-    
     /**
      * Deletes the keyword search core for a case.
      *
@@ -721,46 +720,41 @@ public class Server {
      */
     @NbBundle.Messages({
         "# {0} - core name", "Server.deleteCore.exception.msg=Failed to delete Solr core {0}",})
-    void deleteCore(String coreName) throws KeywordSearchServiceException {
-        /*
-         * Send a core unload request to the Solr server, with the parameters
-         * that request deleting the index and the instance directory
-         * (deleteInstanceDir removes everything related to the core, the index
-         * directory, the configuration files, etc.) set to true.
-         * NOTE: this method doesn't delete the actual Solr index directory. That is 
-         * done as part of deleting case output directory.
-         */
-                
-        // check whether the core we are deleting is the currently open core
-        currentCoreLock.readLock().lock();
+    void deleteCore(String coreName, Case.CaseType caseType) throws KeywordSearchServiceException {
         try {
-            if (null != currentCore) {
-                if (currentCore.getName().equals(coreName)) {
-                    // close current core first
-                    closeCore();
-                }
+            HttpSolrServer solrServer;
+            if (caseType == CaseType.SINGLE_USER_CASE) {
+                Integer localSolrServerPort = Integer.decode(ModuleSettings.getConfigSetting(PROPERTIES_FILE, PROPERTIES_CURRENT_SERVER_PORT));
+                solrServer = new HttpSolrServer("http://localhost:" + localSolrServerPort + "/solr"); //NON-NLS
+            } else {
+                String host = UserPreferences.getIndexingServerHost();
+                String port = UserPreferences.getIndexingServerPort();
+                solrServer = new HttpSolrServer("http://" + host + ":" + port + "/solr"); //NON-NLS
             }
-        } catch (KeywordSearchModuleException ex) {
-            throw new KeywordSearchServiceException(NbBundle.getMessage(Server.class, "Server.close.exception.msg"), ex);
-        } finally {
-            currentCoreLock.readLock().unlock();
-        }
-             
-        try {
-            HttpSolrServer solrServer = new HttpSolrServer("http://" + UserPreferences.getIndexingServerHost() + ":" + UserPreferences.getIndexingServerPort() + "/solr"); //NON-NLS
             connectToSolrServer(solrServer);
-            org.apache.solr.client.solrj.request.CoreAdminRequest.unloadCore(coreName, true, true, solrServer); 
-        } catch (SolrServerException | IOException ex) {
+            CoreAdminResponse response = CoreAdminRequest.getStatus(coreName, solrServer);
+            if (null != response.getCoreStatus(coreName).get("instanceDir")) {             //NON-NLS
+                /*
+                 * Send a core unload request to the Solr server, with the
+                 * parameter set that request deleting the index and the
+                 * instance directory (deleteInstanceDir = true). Note that this
+                 * removes everything related to the core on the server (the
+                 * index directory, the configuration files, etc.), but does not
+                 * delete the actual Solr text index because it is currently
+                 * stored in the case directory.
+                 */
+                org.apache.solr.client.solrj.request.CoreAdminRequest.unloadCore(coreName, true, true, solrServer);
+            }
+        } catch (SolrServerException | HttpSolrServer.RemoteSolrException | IOException ex) {
             throw new KeywordSearchServiceException(Bundle.Server_deleteCore_exception_msg(coreName), ex);
         }
     }
 
-    
     /**
      * Creates/opens a Solr core (index) for a case.
      *
      * @param theCase The case for which the core is to be created/opened.
-     * @param index The text index that the Solr core should be using.
+     * @param index   The text index that the Solr core should be using.
      *
      * @return An object representing the created/opened core.
      *
@@ -780,7 +774,7 @@ public class Server {
             connectToSolrServer(currentSolrServer);
 
         } catch (SolrServerException | IOException ex) {
-            throw new KeywordSearchModuleException(NbBundle.getMessage(Server.class, "Server.connect.exception.msg"), ex);
+            throw new KeywordSearchModuleException(NbBundle.getMessage(Server.class, "Server.connect.exception.msg", ex.getLocalizedMessage()), ex);
         }
 
         try {
@@ -891,8 +885,8 @@ public class Server {
     }
 
     /**
-     * Execute query that gets only number of all Solr file chunks (not
-     * logical files) indexed without actually returning the content.
+     * Execute query that gets only number of all Solr file chunks (not logical
+     * files) indexed without actually returning the content.
      *
      * @return int representing number of indexed chunks
      *
@@ -916,8 +910,8 @@ public class Server {
     }
 
     /**
-     * Execute query that gets only number of all Solr documents indexed
-     * (files and chunks) without actually returning the documents
+     * Execute query that gets only number of all Solr documents indexed (files
+     * and chunks) without actually returning the documents
      *
      * @return int representing number of indexed files (files and chunks)
      *
@@ -970,11 +964,10 @@ public class Server {
     /**
      * Execute query that gets number of indexed file chunks for a file
      *
-     * @param fileID file id of the original file broken into chunks and
-     *               indexed
+     * @param fileID file id of the original file broken into chunks and indexed
      *
-     * @return int representing number of indexed file chunks, 0 if there is
-     *         no chunks
+     * @return int representing number of indexed file chunks, 0 if there is no
+     *         chunks
      *
      * @throws KeywordSearchModuleException
      * @throws NoOpenCoreException
@@ -1206,8 +1199,7 @@ public class Server {
     }
 
     /**
-     * Determines whether or not the index files folder for a Solr core
-     * exists.
+     * Determines whether or not the index files folder for a Solr core exists.
      *
      * @param coreName the name of the core.
      *
@@ -1233,7 +1225,7 @@ public class Server {
         private final String name;
 
         private final CaseType caseType;
-        
+
         private final Index textIndex;
 
         // the server to access a core needs to be built from a URL with the
@@ -1272,7 +1264,7 @@ public class Server {
         private Index getIndexInfo() {
             return this.textIndex;
         }
-        
+
         private QueryResponse query(SolrQuery sq) throws SolrServerException, IOException {
             return solrCore.query(sq);
         }
@@ -1385,8 +1377,8 @@ public class Server {
          * Execute query that gets only number of all Solr files (not chunks)
          * indexed without actually returning the files
          *
-         * @return int representing number of indexed files (entire files,
-         *         not chunks)
+         * @return int representing number of indexed files (entire files, not
+         *         chunks)
          *
          * @throws SolrServerException
          */
@@ -1447,11 +1439,11 @@ public class Server {
         /**
          * Execute query that gets number of indexed file chunks for a file
          *
-         * @param contentID file id of the original file broken into chunks
-         *                  and indexed
+         * @param contentID file id of the original file broken into chunks and
+         *                  indexed
          *
-         * @return int representing number of indexed file chunks, 0 if there
-         *         is no chunks
+         * @return int representing number of indexed file chunks, 0 if there is
+         *         no chunks
          *
          * @throws SolrServerException
          */
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/SolrSearchService.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/SolrSearchService.java
index acde9d77fc..eee3ac4dad 100644
--- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/SolrSearchService.java
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/SolrSearchService.java
@@ -18,6 +18,7 @@
  */
 package org.sleuthkit.autopsy.keywordsearch;
 
+import java.io.File;
 import java.io.IOException;
 import java.lang.reflect.InvocationTargetException;
 import java.net.InetAddress;
@@ -37,9 +38,10 @@ import org.openide.util.lookup.ServiceProviders;
 import org.sleuthkit.autopsy.casemodule.Case;
 import org.sleuthkit.autopsy.casemodule.CaseMetadata;
 import org.sleuthkit.autopsy.core.RuntimeProperties;
+import org.sleuthkit.autopsy.coreutils.FileUtil;
 import org.sleuthkit.autopsy.coreutils.Logger;
-import org.sleuthkit.autopsy.framework.AutopsyService;
-import org.sleuthkit.autopsy.framework.ProgressIndicator;
+import org.sleuthkit.autopsy.appservices.AutopsyService;
+import org.sleuthkit.autopsy.progress.ProgressIndicator;
 import org.sleuthkit.autopsy.keywordsearchservice.KeywordSearchService;
 import org.sleuthkit.autopsy.keywordsearchservice.KeywordSearchServiceException;
 import org.sleuthkit.datamodel.BlackboardArtifact;
@@ -50,7 +52,8 @@ import org.sleuthkit.datamodel.TskCoreException;
  * text indexing and search.
  */
 @ServiceProviders(value = {
-    @ServiceProvider(service = KeywordSearchService.class),
+    @ServiceProvider(service = KeywordSearchService.class)
+    ,
     @ServiceProvider(service = AutopsyService.class)}
 )
 public class SolrSearchService implements KeywordSearchService, AutopsyService {
@@ -138,16 +141,17 @@ public class SolrSearchService implements KeywordSearchService, AutopsyService {
         }
     }
 
-    @NbBundle.Messages({"# {0} - case directory",
-        "SolrSearchService.exceptionMessage.noIndexMetadata=Unable to create IndexMetaData from caseDirectory: {0}",
-        "# {0} - case directory",
-        "SolrSearchService.exceptionMessage.noCurrentSolrCore=IndexMetadata did not contain a current Solr core so could not delete the case"
-    })
     /**
      * Deletes Solr core for a case.
      *
      * @param metadata The CaseMetadata which will have its core deleted.
      */
+    @NbBundle.Messages({"# {0} - case directory",
+        "SolrSearchService.exceptionMessage.noIndexMetadata=Unable to create IndexMetaData from caseDirectory: {0}",
+        "# {0} - case directory",
+        "SolrSearchService.exceptionMessage.noCurrentSolrCore=IndexMetadata did not contain a current Solr core so could not delete the case",
+        "SolrSearchService.exceptionMessage.failedToDeleteIndexFiles=Failed to delete text index files at {0}"
+    })
     @Override
     public void deleteTextIndex(CaseMetadata metadata) throws KeywordSearchServiceException {
         String caseDirectory = metadata.getCaseDirectory();
@@ -163,13 +167,23 @@ public class SolrSearchService implements KeywordSearchService, AutopsyService {
         String currentSolr = IndexFinder.getCurrentSolrVersion();
         for (Index index : indexMetadata.getIndexes()) {
             if (index.getSolrVersion().equals(currentSolr) && index.getSchemaVersion().equals(currentSchema)) {
-                KeywordSearch.getServer().deleteCore(index.getIndexName());
-                return; //only one core exists for each combination of solr and schema version
+                /*
+                 * Unload/delete the core on the server and then delete the text
+                 * index files.
+                 */
+                KeywordSearch.getServer().deleteCore(index.getIndexName(), metadata.getCaseType());
+                if (!FileUtil.deleteDir(new File(index.getIndexPath()).getParentFile())) {
+                    throw new KeywordSearchServiceException(Bundle.SolrSearchService_exceptionMessage_failedToDeleteIndexFiles(index.getIndexPath()));                    
+                }
             }
+            return; //only one core exists for each combination of solr and schema version
         }
+
         //this code this code will only execute if an index for the current core was not found 
-        logger.log(Level.WARNING, NbBundle.getMessage(SolrSearchService.class, "SolrSearchService.exceptionMessage.noCurrentSolrCore"));
-        throw new KeywordSearchServiceException(NbBundle.getMessage(SolrSearchService.class, "SolrSearchService.exceptionMessage.noCurrentSolrCore"));
+        logger.log(Level.WARNING, NbBundle.getMessage(SolrSearchService.class,
+                 "SolrSearchService.exceptionMessage.noCurrentSolrCore"));
+        throw new KeywordSearchServiceException(NbBundle.getMessage(SolrSearchService.class,
+                 "SolrSearchService.exceptionMessage.noCurrentSolrCore"));
     }
 
     @Override
@@ -200,6 +214,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;
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/StringsTextExtractor.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/StringsTextExtractor.java
index 940c5d0f76..4ccc8d76c5 100644
--- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/StringsTextExtractor.java
+++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/StringsTextExtractor.java
@@ -47,7 +47,7 @@ class StringsTextExtractor extends FileTextExtractor {
         EXTRACT_UTF16, ///< extract UTF16 text, true/false
         EXTRACT_UTF8, ///< extract UTF8 text, true/false
     };
-    
+
     private final List