diff --git a/.gitignore b/.gitignore index b89812646f..9ae14c5cac 100644 --- a/.gitignore +++ b/.gitignore @@ -65,7 +65,6 @@ genfiles.properties !/Testing/nbproject/project.properties *~ /netbeans-plat -/docs/doxygen/doxygen_docs /docs/doxygen-user/user-docs /jdiff-javadocs/* /jdiff-logs/* diff --git a/BUILDING.txt b/BUILDING.txt index 7361735adf..c08806503c 100644 --- a/BUILDING.txt +++ b/BUILDING.txt @@ -1,8 +1,8 @@ - Last Updated: Sept 17, 2014 + Last Updated: 27 April 2015 This file outlines what it takes to build Autopsy from source. -Note that it currently only works out of the box on Windows. We +Note that it currently only works out-of-the-box on Windows. We are working on getting the process working under non-Windows systems. It generally works, but needs some custom mangling to find the correct C libraries. @@ -11,7 +11,10 @@ correct C libraries. STEPS: 1) Get Java Setup -1a) Download and install JDK version 1.8. For the current version of JavaFX that we use, you'll need 1.8.0_40 or greater. You can now use 32-bit or 64-bit, but special work is needed to get The Sleuth Kit to compile as 64-bit. So, 32-bit is easier. +1a) Download and install JDK version 1.8. For the current version of JavaFX +that we use, you'll need 1.8.0_40 or greater. You can now use 32-bit or 64-bit, +but special work is needed to get The Sleuth Kit to compile as 64-bit. So, +32-bit is easier, but if you intend to use PostgreSQL, choose 64-bit. Autopsy has been used and tested with Oracle JavaSE and the included JavaFX support (http://www.oracle.com/technetwork/java/javase/downloads/index.html). @@ -28,33 +31,25 @@ but it is a recommended IDE to use for development of Autopsy modules. need to set JRE_HOME_32 to the root 32-bit JRE directory and/or JRE_HOME_64 to the root 64-bit JRE directory. -1e) (optional) For some Autopsy features to be functional, you need to add java executable to the system PATH. - +1e) (optional) For some Autopsy features to be functional, you need to add the + java executable to the system PATH. 2) Get Sleuth Kit Setup -2a) Download and build the release version of Libewf2 (20130119 or -later). All you need is the dll file. Note that you will get a -launching error if you use libewf 1. -- http://sourceforge.net/projects/libewf/ -If you want to build the 64-bit version of The Sleuth Kit, download -our 64-bit version of libewf: -- https://github.com/sleuthkit/libewf_64bit - -2b) Set LIBEWF_HOME environment variable to root directory of LIBEWF - -2c) Download and build a Release version of Sleuth Kit (TSK) 4.0. You -need to build the tsk_jni project. You can use a released version or -download the latest from github: +2a) Download and build a Release version of Sleuth Kit (TSK) 4.0. See + win32\BUILDING.txt in the TSK package for more information. You need to + build the tsk_jni project. Select the Release_PostgreSQL x64 target. You can + use a released version or download the latest from github: - git://github.com/sleuthkit/sleuthkit.git -2d) Build the TSK JAR file by typing 'ant' in bindings/java in the -TSK source code folder from a command line. You can also add the -code to a NetBeans project and build it from there. +2b) Build the TSK JAR file by typing 'ant PostgreSQL' in bindings/java in the + TSK source code folder from a command line. Note it is case sensitive. You + can also add the code to a NetBeans project and build it from there, + selecting the PostgreSQL target. -2e) Set TSK_HOME environment variable to the root directory of TSK +2c) Set TSK_HOME environment variable to the root directory of TSK -2f) On Non-Windows systems, you will need to do a 'make install' +2d) On Non-Windows systems, you will need to do a 'make install' from the TSK root directory to install the libraries and such in the needed places (i.e. '/usr/local'). @@ -85,12 +80,12 @@ and by submitting pull requests to the main Autopsy repository. 5) Compile Autopsy 5a) using Netbeans IDE: - Start NetBeans IDE and open the Autopsy project. -- Choose to build the Autopsy project / module. It is the highest -level project that will then cause the other modules to be compiled. +- Choose to build the Autopsy project / module. It is the highest level project + that will cause the other modules to be compiled. 5b) without Netbeans IDE (requires JDK and ant >= 1.7.1): - from root directory of Autopsy source execute: -ant build +ant (to build Autopsy) ant run (to run Autopsy) @@ -101,14 +96,13 @@ Here are some notes to shed some light on what is going on during the build process. - The Sleuth Kit Java datamodel JAR file has native JNI libraries -that are copied into it. These JNI libraries have dependencies on -libewf and zlib. On non-Windows platforms, the JNI library also has -a dependency on libtsk (on Windows, it is compiled into libtsk_jni). +that are copied into it. These JNI libraries have dependencies on +libewf, zlib, libpq, libintl-8, libeay32, and ssleay32 DLL files. On non-Windows +platforms, the JNI library also has a dependency on libtsk (on Windows, +it is compiled into libtsk_jni). -- NetBeans uses ant to build Autopsy. The build target copies the -TSK datamodel JAR file into the project. If you want to use the -debug version of the TSK dll, then there is a different ant target -in the TSK datamodel to copy the debug versions of the dlls. +- NetBeans uses ant to build Autopsy. The build target copies the +TSK datamodel JAR file into the project. - On a Windows system, the compile-time ant target copies the dependency libraries into the Autopsy code structure so that they can diff --git a/Core/build.xml b/Core/build.xml index 128d0c1286..c0936f07e8 100644 --- a/Core/build.xml +++ b/Core/build.xml @@ -24,6 +24,10 @@ + + + + diff --git a/Core/nbproject/project.properties b/Core/nbproject/project.properties index 9cdbb08f9e..86ed71097f 100644 --- a/Core/nbproject/project.properties +++ b/Core/nbproject/project.properties @@ -1,19 +1,21 @@ +file.reference.activemq-all-5.11.1.jar=release/modules/ext/activemq-all-5.11.1.jar file.reference.jdom-2.0.5-contrib.jar=release/modules/ext/jdom-2.0.5-contrib.jar file.reference.jdom-2.0.5.jar=release/modules/ext/jdom-2.0.5.jar file.reference.jython-standalone-2.7.0.jar=release/modules/ext/jython-standalone-2.7.0.jar -file.reference.jython.jar-1=release/modules/ext/jython.jar file.reference.metadata-extractor-2.8.1.jar=release/modules/ext/metadata-extractor-2.8.1.jar +file.reference.postgresql-9.4-1201-jdbc41.jar=release/modules/ext/postgresql-9.4-1201-jdbc41.jar file.reference.opencv-248.jar=release/modules/ext/opencv-248.jar file.reference.Rejistry-1.0-SNAPSHOT.jar=release/modules/ext/Rejistry-1.0-SNAPSHOT.jar file.reference.sevenzipjbinding-AllPlatforms.jar=release/modules/ext/sevenzipjbinding-AllPlatforms.jar 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.2.jar=release/modules/ext/tika-core-1.2.jar +file.reference.tika-core-1.5.jar=release/modules/ext/tika-core-1.5.jar file.reference.Tsk_DataModel.jar=release/modules/ext/Tsk_DataModel.jar file.reference.xmpcore-5.1.2.jar=release/modules/ext/xmpcore-5.1.2.jar javac.source=1.8 javac.compilerargs=-Xlint -Xlint:-serial +javadoc.reference.metadata-extractor-2.8.1.jar=release/modules/ext/metadata-extractor-2.8.1-src.zip license.file=../LICENSE-2.0.txt nbm.homepage=http://www.sleuthkit.org/ nbm.module.author=Brian Carrier diff --git a/Core/nbproject/project.xml b/Core/nbproject/project.xml index c8089bda6c..78bb7e701e 100644 --- a/Core/nbproject/project.xml +++ b/Core/nbproject/project.xml @@ -184,9 +184,11 @@ org.sleuthkit.autopsy.actions org.sleuthkit.autopsy.casemodule + org.sleuthkit.autopsy.casemodule.events org.sleuthkit.autopsy.casemodule.services org.sleuthkit.autopsy.contentviewers org.sleuthkit.autopsy.core + org.sleuthkit.autopsy.core.events org.sleuthkit.autopsy.corecomponentinterfaces org.sleuthkit.autopsy.corecomponents org.sleuthkit.autopsy.coreutils @@ -203,14 +205,26 @@ org.sleuthkit.autopsy.report org.sleuthkit.datamodel - - ext/xmpcore-5.1.2.jar - release/modules/ext/xmpcore-5.1.2.jar - ext/jdom-2.0.5.jar release/modules/ext/jdom-2.0.5.jar + + ext/postgresql-9.4-1201-jdbc41.jar + release/modules/ext/postgresql-9.4-1201-jdbc41.jar + + + ext/mchange-commons-java-0.2.9.jar + release/modules/ext/mchange-commons-java-0.2.9.jar + + + ext/c3p0-0.9.5.jar + release/modules/ext/c3p0-0.9.5.jar + + + ext/xmpcore-5.1.2.jar + release/modules/ext/xmpcore-5.1.2.jar + ext/StixLib.jar release/modules/ext/StixLib.jar @@ -228,20 +242,32 @@ release/modules/ext/Rejistry-1.0-SNAPSHOT.jar - ext/sevenzipjbinding.jar - release/modules/ext/sevenzipjbinding.jar + ext/activemq-all-5.11.1.jar + release/modules/ext/activemq-all-5.11.1.jar + + + ext/Rejistry-1.0-SNAPSHOT.jar + release/modules/ext/Rejistry-1.0-SNAPSHOT.jar ext/jython-standalone-2.7.0.jar release/modules/ext/jython-standalone-2.7.0.jar + + ext/sevenzipjbinding.jar + release/modules/ext/sevenzipjbinding.jar + ext/sevenzipjbinding-AllPlatforms.jar release/modules/ext/sevenzipjbinding-AllPlatforms.jar - ext/tika-core-1.2.jar - release/modules/ext/tika-core-1.2.jar + ext/tika-core-1.5.jar + release/modules/ext/tika-core-1.5.jar + + + ext/metadata-extractor-2.8.1.jar + release/modules/ext/metadata-extractor-2.8.1.jar ext/metadata-extractor-2.8.1.jar diff --git a/Core/release/modules/ext/activemq-all-5.11.1.jar b/Core/release/modules/ext/activemq-all-5.11.1.jar new file mode 100644 index 0000000000..59eff437fa Binary files /dev/null and b/Core/release/modules/ext/activemq-all-5.11.1.jar differ diff --git a/Core/release/modules/ext/metadata-extractor-2.8.1-src.zip b/Core/release/modules/ext/metadata-extractor-2.8.1-src.zip new file mode 100755 index 0000000000..38c449d420 Binary files /dev/null and b/Core/release/modules/ext/metadata-extractor-2.8.1-src.zip differ diff --git a/Core/release/modules/ext/tika-core-1.2.jar b/Core/release/modules/ext/tika-core-1.2.jar deleted file mode 100755 index e1491ab5f2..0000000000 Binary files a/Core/release/modules/ext/tika-core-1.2.jar and /dev/null differ diff --git a/Core/release/modules/ext/tika-core-1.5.jar b/Core/release/modules/ext/tika-core-1.5.jar new file mode 100644 index 0000000000..d8e45e3f28 Binary files /dev/null and b/Core/release/modules/ext/tika-core-1.5.jar differ diff --git a/Core/src/org/sleuthkit/autopsy/actions/AddBlackboardArtifactTagAction.java b/Core/src/org/sleuthkit/autopsy/actions/AddBlackboardArtifactTagAction.java index 73121b13dc..2111f3a554 100755 --- a/Core/src/org/sleuthkit/autopsy/actions/AddBlackboardArtifactTagAction.java +++ b/Core/src/org/sleuthkit/autopsy/actions/AddBlackboardArtifactTagAction.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013 Basis Technology Corp. + * Copyright 2013-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,7 +21,7 @@ package org.sleuthkit.autopsy.actions; import java.util.Collection; import java.util.logging.Level; import javax.swing.JOptionPane; - +import javax.swing.SwingUtilities; import org.openide.util.NbBundle; import org.openide.util.Utilities; import org.sleuthkit.autopsy.casemodule.Case; @@ -63,20 +63,25 @@ public class AddBlackboardArtifactTagAction extends AddTagAction { @Override protected void addTag(TagName tagName, String comment) { - Collection selectedArtifacts = Utilities.actionsGlobalContext().lookupAll(BlackboardArtifact.class); - for (BlackboardArtifact artifact : selectedArtifacts) { - try { - Case.getCurrentCase().getServices().getTagsManager().addBlackboardArtifactTag(artifact, tagName, comment); - } catch (TskCoreException ex) { - Logger.getLogger(AddBlackboardArtifactTagAction.class.getName()).log(Level.SEVERE, "Error tagging result", ex); //NON-NLS - JOptionPane.showMessageDialog(null, - NbBundle.getMessage(this.getClass(), - "AddBlackboardArtifactTagAction.unableToTag.msg", - artifact.getDisplayName()), - NbBundle.getMessage(this.getClass(), - "AddBlackboardArtifactTagAction.taggingErr"), - JOptionPane.ERROR_MESSAGE); + final Collection selectedArtifacts = Utilities.actionsGlobalContext().lookupAll(BlackboardArtifact.class); + + new Thread(() -> { + for (BlackboardArtifact artifact : selectedArtifacts) { + try { + Case.getCurrentCase().getServices().getTagsManager().addBlackboardArtifactTag(artifact, tagName, comment); + } catch (TskCoreException ex) { + Logger.getLogger(AddBlackboardArtifactTagAction.class.getName()).log(Level.SEVERE, "Error tagging result", ex); //NON-NLS + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog(null, + NbBundle.getMessage(this.getClass(), + "AddBlackboardArtifactTagAction.unableToTag.msg", + artifact.getDisplayName()), + NbBundle.getMessage(this.getClass(), + "AddBlackboardArtifactTagAction.taggingErr"), + JOptionPane.ERROR_MESSAGE); + }); + } } - } + }).start(); } } diff --git a/Core/src/org/sleuthkit/autopsy/actions/AddContentTagAction.java b/Core/src/org/sleuthkit/autopsy/actions/AddContentTagAction.java index 47c0f69c0c..b1ea2b31f4 100755 --- a/Core/src/org/sleuthkit/autopsy/actions/AddContentTagAction.java +++ b/Core/src/org/sleuthkit/autopsy/actions/AddContentTagAction.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-15 Basis Technology Corp. + * Copyright 2013-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +22,7 @@ import java.util.Collection; import java.util.List; import java.util.logging.Level; import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; import org.openide.util.NbBundle; import org.openide.util.Utilities; import org.sleuthkit.autopsy.casemodule.Case; @@ -41,6 +42,7 @@ public class AddContentTagAction extends AddTagAction { // This class is a singleton to support multi-selection of nodes, since // org.openide.nodes.NodeOp.findActions(Node[] nodes) will only pick up an Action if every // node in the array returns a reference to the same action object from Node.getActions(boolean). + private static AddContentTagAction instance; public static synchronized AddContentTagAction getInstance() { @@ -63,71 +65,93 @@ public class AddContentTagAction extends AddTagAction { @Override protected void addTag(TagName tagName, String comment) { - Collection selectedFiles = Utilities.actionsGlobalContext().lookupAll(AbstractFile.class); - for (AbstractFile file : selectedFiles) { - try { - // Handle the special cases of current (".") and parent ("..") directory entries. - if (".".equals(file.getName())) { - Content parentFile = file.getParent(); - if (parentFile instanceof AbstractFile) { - file = (AbstractFile) parentFile; - } else { - JOptionPane.showMessageDialog(null, - NbBundle.getMessage(this.getClass(), - "AddContentTagAction.unableToTag.msg", - parentFile.getName()), - NbBundle.getMessage(this.getClass(), - "AddContentTagAction.cannotApplyTagErr"), - JOptionPane.WARNING_MESSAGE); - continue; - } - } else if ("..".equals(file.getName())) { - Content parentFile = file.getParent(); - if (parentFile instanceof AbstractFile) { - parentFile = parentFile.getParent(); + final Collection selectedFiles = Utilities.actionsGlobalContext().lookupAll(AbstractFile.class); + + new Thread(() -> { + for (AbstractFile file : selectedFiles) { + try { + // Handle the special cases of current (".") and parent ("..") directory entries. + if (file.getName().equals(".")) { + Content parentFile = file.getParent(); if (parentFile instanceof AbstractFile) { file = (AbstractFile) parentFile; } else { - JOptionPane.showMessageDialog(null, - NbBundle.getMessage(this.getClass(), - "AddContentTagAction.unableToTag.msg", - parentFile.getName()), - NbBundle.getMessage(this.getClass(), - "AddContentTagAction.cannotApplyTagErr"), - JOptionPane.WARNING_MESSAGE); + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog(null, + NbBundle.getMessage(this.getClass(), + "AddContentTagAction.unableToTag.msg", + parentFile.getName()), + NbBundle.getMessage(this.getClass(), + "AddContentTagAction.cannotApplyTagErr"), + JOptionPane.WARNING_MESSAGE); + }); continue; } - } else { + } else if (file.getName().equals("..")) { + Content parentFile = file.getParent(); + if (parentFile instanceof AbstractFile) { + parentFile = (AbstractFile) ((AbstractFile) parentFile).getParent(); + if (parentFile instanceof AbstractFile) { + file = (AbstractFile) parentFile; + } else { + final Content parentFileCopy = parentFile; + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog(null, + NbBundle.getMessage(this.getClass(), + "AddContentTagAction.unableToTag.msg", + parentFileCopy.getName()), + NbBundle.getMessage(this.getClass(), + "AddContentTagAction.cannotApplyTagErr"), + JOptionPane.WARNING_MESSAGE); + }); + continue; + } + } else { + final Content parentFileCopy = parentFile; + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog(null, + NbBundle.getMessage(this.getClass(), + "AddContentTagAction.unableToTag.msg", + parentFileCopy.getName()), + NbBundle.getMessage(this.getClass(), + "AddContentTagAction.cannotApplyTagErr"), + JOptionPane.WARNING_MESSAGE); + }); + continue; + } + } + // check if the same tag is being added for the same abstract file. + TagsManager tagsManager = Case.getCurrentCase().getServices().getTagsManager(); + List contentTagList = tagsManager.getContentTagsByContent(file); + for (ContentTag contentTag : contentTagList) { + if (contentTag.getName().getDisplayName().equals(tagName.getDisplayName())) { + AbstractFile fileCopy = file; + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog(null, + NbBundle.getMessage(this.getClass(), + "AddContentTagAction.tagExists", + fileCopy.getName(), tagName.getDisplayName()), + NbBundle.getMessage(this.getClass(), + "AddContentTagAction.cannotApplyTagErr"), + JOptionPane.WARNING_MESSAGE); + }); + return; + } + } + tagsManager.addContentTag(file, tagName, comment); + } catch (TskCoreException ex) { + Logger.getLogger(AddContentTagAction.class.getName()).log(Level.SEVERE, "Error tagging result", ex); //NON-NLS + AbstractFile fileCopy = file; + SwingUtilities.invokeLater(() -> { JOptionPane.showMessageDialog(null, NbBundle.getMessage(this.getClass(), - "AddContentTagAction.unableToTag.msg", - parentFile.getName()), - NbBundle.getMessage(this.getClass(), - "AddContentTagAction.cannotApplyTagErr"), - JOptionPane.WARNING_MESSAGE); - continue; - } + "AddContentTagAction.unableToTag.msg2", + fileCopy.getName()), + NbBundle.getMessage(this.getClass(), "AddContentTagAction.taggingErr"), + JOptionPane.ERROR_MESSAGE); + }); } - // check if the same tag is being added for the same abstract file. - TagsManager tagsManager = Case.getCurrentCase().getServices().getTagsManager(); - List contentTagList = tagsManager.getContentTagsByContent(file); - boolean alreadyTaggedWithTagName = contentTagList.stream() - .map(ContentTag::getName) - .filter(tagName::equals) - .findAny().isPresent(); - if (alreadyTaggedWithTagName) { - continue; //skip this file, it already has a tag with this TagName. - } - tagsManager.addContentTag(file, tagName, comment); - } catch (TskCoreException ex) { - Logger.getLogger(AddContentTagAction.class.getName()).log(Level.SEVERE, "Error tagging result", ex); //NON-NLS - JOptionPane.showMessageDialog(null, - NbBundle.getMessage(this.getClass(), - "AddContentTagAction.unableToTag.msg2", - file.getName()), - NbBundle.getMessage(this.getClass(), "AddContentTagAction.taggingErr"), - JOptionPane.ERROR_MESSAGE); } - } + }).start(); } } diff --git a/Core/src/org/sleuthkit/autopsy/actions/Bundle.properties b/Core/src/org/sleuthkit/autopsy/actions/Bundle.properties index ca5c8b3174..832079a515 100755 --- a/Core/src/org/sleuthkit/autopsy/actions/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/actions/Bundle.properties @@ -49,7 +49,12 @@ GetTagNameDialog.taggingErr=Tagging Error GetTagNameDialog.tagNameAlreadyDef.msg=A {0} tag name has already been defined. GetTagNameDialog.dupTagErr=Duplicate Tag Error OpenLogFolder.error1=Log File Not Found: {0} +OpenLogFolder.CouldNotOpenLogFolder=Could not open log folder CTL_OpenLogFolder=Open Log Folder +CTL_OpenOutputFolder=Open Output Folder +OpenOutputFolder.error1=Output Folder Not Found: {0} +OpenOutputFolder.noCaseOpen=No open case, therefore no current output folder available. +OpenOutputFolder.CouldNotOpenOutputFolder=Could not open output folder ShowIngestProgressSnapshotAction.actionName.text=Get Ingest Progress Snapshot OpenPythonModulesFolderAction.actionName.text=Python Plugins OpenPythonModulesFolderAction.errorMsg.folderNotFound=Python plugins folder not found: {0} diff --git a/Core/src/org/sleuthkit/autopsy/actions/DeleteBlackboardArtifactTagAction.java b/Core/src/org/sleuthkit/autopsy/actions/DeleteBlackboardArtifactTagAction.java index beaea6dc1d..ae3c8c52e5 100755 --- a/Core/src/org/sleuthkit/autopsy/actions/DeleteBlackboardArtifactTagAction.java +++ b/Core/src/org/sleuthkit/autopsy/actions/DeleteBlackboardArtifactTagAction.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013 Basis Technology Corp. + * Copyright 2013-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,6 +23,7 @@ import java.util.Collection; import java.util.logging.Level; import javax.swing.AbstractAction; import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; import org.openide.util.NbBundle; import org.openide.util.Utilities; import org.sleuthkit.autopsy.casemodule.Case; @@ -36,6 +37,7 @@ import org.sleuthkit.datamodel.TskCoreException; */ public class DeleteBlackboardArtifactTagAction extends AbstractAction { + private static final long serialVersionUID = 1L; private static final String MENU_TEXT = NbBundle.getMessage(DeleteBlackboardArtifactTagAction.class, "DeleteBlackboardArtifactTagAction.deleteTags"); @@ -57,20 +59,49 @@ public class DeleteBlackboardArtifactTagAction extends AbstractAction { @Override public void actionPerformed(ActionEvent event) { - Collection selectedTags = Utilities.actionsGlobalContext().lookupAll(BlackboardArtifactTag.class); - for (BlackboardArtifactTag tag : selectedTags) { - try { - Case.getCurrentCase().getServices().getTagsManager().deleteBlackboardArtifactTag(tag); - } catch (TskCoreException ex) { - Logger.getLogger(AddContentTagAction.class.getName()).log(Level.SEVERE, "Error deleting tag", ex); //NON-NLS - JOptionPane.showMessageDialog(null, - NbBundle.getMessage(this.getClass(), - "DeleteBlackboardArtifactTagAction.unableToDelTag.msg", - tag.getName()), - NbBundle.getMessage(this.getClass(), - "DeleteBlackboardArtifactTagAction.tagDelErr"), - JOptionPane.ERROR_MESSAGE); + final Collection selectedTags = Utilities.actionsGlobalContext().lookupAll(BlackboardArtifactTag.class); + new Thread(() -> { + for (BlackboardArtifactTag tag : selectedTags) { + try { + Case.getCurrentCase().getServices().getTagsManager().deleteBlackboardArtifactTag(tag); + } catch (TskCoreException ex) { + Logger.getLogger(AddContentTagAction.class.getName()).log(Level.SEVERE, "Error deleting tag", ex); //NON-NLS + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog(null, + NbBundle.getMessage(this.getClass(), + "DeleteBlackboardArtifactTagAction.unableToDelTag.msg", + tag.getName()), + NbBundle.getMessage(this.getClass(), + "DeleteBlackboardArtifactTagAction.tagDelErr"), + JOptionPane.ERROR_MESSAGE); + }); + } } - } + }).start(); } + + /** + * Deprecated, use actionPerformed() instead. + * + * @param event The event associated with the action. + * + * @deprecated + */ + @Deprecated + protected void doAction(ActionEvent event) { + actionPerformed(event); + } + + /** + * Deprecated, does nothing. The TagManager methods to create, update or + * delete tags now notify the case that there is a tag change. The case then + * publishes an event that triggers a refresh of the tags sub-tree in the + * tree view. + * + * @deprecated + */ + @Deprecated + protected void refreshDirectoryTree() { + } + } diff --git a/Core/src/org/sleuthkit/autopsy/actions/DeleteContentTagAction.java b/Core/src/org/sleuthkit/autopsy/actions/DeleteContentTagAction.java index 88dac00654..c37eedd024 100755 --- a/Core/src/org/sleuthkit/autopsy/actions/DeleteContentTagAction.java +++ b/Core/src/org/sleuthkit/autopsy/actions/DeleteContentTagAction.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013 Basis Technology Corp. + * Copyright 2013-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,6 +23,7 @@ import java.util.Collection; import java.util.logging.Level; import javax.swing.AbstractAction; import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; import org.openide.util.NbBundle; import org.openide.util.Utilities; import org.sleuthkit.autopsy.casemodule.Case; @@ -35,6 +36,7 @@ import org.sleuthkit.datamodel.TskCoreException; */ public class DeleteContentTagAction extends AbstractAction { + private static final long serialVersionUID = 1L; private static final String MENU_TEXT = NbBundle.getMessage(DeleteContentTagAction.class, "DeleteContentTagAction.deleteTags"); @@ -56,19 +58,48 @@ public class DeleteContentTagAction extends AbstractAction { @Override public void actionPerformed(ActionEvent e) { - Collection selectedTags = Utilities.actionsGlobalContext().lookupAll(ContentTag.class); - for (ContentTag tag : selectedTags) { - try { - Case.getCurrentCase().getServices().getTagsManager().deleteContentTag(tag); - } catch (TskCoreException ex) { - Logger.getLogger(AddContentTagAction.class.getName()).log(Level.SEVERE, "Error deleting tag", ex); //NON-NLS - JOptionPane.showMessageDialog(null, - NbBundle.getMessage(this.getClass(), - "DeleteContentTagAction.unableToDelTag.msg", - tag.getName()), - NbBundle.getMessage(this.getClass(), "DeleteContentTagAction.tagDelErr"), - JOptionPane.ERROR_MESSAGE); + final Collection selectedTags = Utilities.actionsGlobalContext().lookupAll(ContentTag.class); + new Thread(() -> { + for (ContentTag tag : selectedTags) { + try { + Case.getCurrentCase().getServices().getTagsManager().deleteContentTag(tag); + } catch (TskCoreException ex) { + Logger.getLogger(AddContentTagAction.class.getName()).log(Level.SEVERE, "Error deleting tag", ex); //NON-NLS + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog(null, + NbBundle.getMessage(this.getClass(), + "DeleteContentTagAction.unableToDelTag.msg", + tag.getName()), + NbBundle.getMessage(this.getClass(), "DeleteContentTagAction.tagDelErr"), + JOptionPane.ERROR_MESSAGE); + }); + } } - } + }).start(); } + + /** + * Deprecated, use actionPerformed() instead. + * + * @param event The event associated with the action. + * + * @deprecated + */ + @Deprecated + protected void doAction(ActionEvent event) { + actionPerformed(event); + } + + /** + * Deprecated, does nothing. The TagManager methods to create, update or + * delete tags now notify the case that there is a tag change. The case then + * publishes an event that triggers a refresh of the tags sub-tree in the + * tree view. + * + * @deprecated + */ + @Deprecated + protected void refreshDirectoryTree() { + } + } diff --git a/Core/src/org/sleuthkit/autopsy/actions/OpenLogFolderAction.java b/Core/src/org/sleuthkit/autopsy/actions/OpenLogFolderAction.java index 014d2d4299..bbadf4291d 100755 --- a/Core/src/org/sleuthkit/autopsy/actions/OpenLogFolderAction.java +++ b/Core/src/org/sleuthkit/autopsy/actions/OpenLogFolderAction.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014 Basis Technology Corp. + * Copyright 2014-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,15 +23,16 @@ import java.awt.event.ActionListener; import java.awt.Desktop; import java.io.File; import java.io.IOException; +import java.util.logging.Level; import org.openide.DialogDisplayer; import org.openide.NotifyDescriptor; import org.openide.awt.ActionID; import org.openide.awt.ActionReference; import org.openide.awt.ActionRegistration; import org.openide.modules.Places; -import org.openide.util.Exceptions; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; /** * Action in menu to open the folder containing the log files @@ -42,6 +43,8 @@ import org.sleuthkit.autopsy.casemodule.Case; @ActionID(id = "org.sleuthkit.autopsy.actions.OpenLogFolderAction", category = "Help") public final class OpenLogFolderAction implements ActionListener { + private static final Logger logger = Logger.getLogger(OpenLogFolderAction.class.getName()); + @Override public void actionPerformed(ActionEvent e) { try { @@ -61,7 +64,8 @@ public final class OpenLogFolderAction implements ActionListener { Desktop.getDesktop().open(logDir); } } catch (IOException ex) { - Exceptions.printStackTrace(ex); + logger.log(Level.WARNING, NbBundle.getMessage(this.getClass(), "OpenLogFolder.CouldNotOpenLogFolder"), ex); //NON-NLS + } } } diff --git a/Core/src/org/sleuthkit/autopsy/actions/OpenOutputFolderAction.java b/Core/src/org/sleuthkit/autopsy/actions/OpenOutputFolderAction.java new file mode 100644 index 0000000000..2ea5c95538 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/actions/OpenOutputFolderAction.java @@ -0,0 +1,71 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.actions; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.Desktop; +import java.io.File; +import java.io.IOException; +import java.util.logging.Level; +import javax.swing.JOptionPane; +import org.openide.DialogDisplayer; +import org.openide.NotifyDescriptor; +import org.openide.awt.ActionID; +import org.openide.awt.ActionReference; +import org.openide.awt.ActionRegistration; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; + +/** + * Action in menu to open the folder containing the output files + */ +@ActionRegistration( + displayName = "#CTL_OpenOutputFolder", iconInMenu = true) +@ActionReference(path = "Menu/Help", position = 1850) +@ActionID(id = "org.sleuthkit.autopsy.actions.OpenOutputFolderAction", category = "Help") +public final class OpenOutputFolderAction implements ActionListener { + + private static final Logger logger = Logger.getLogger(OpenOutputFolderAction.class.getName()); + + @Override + public void actionPerformed(ActionEvent e) { + + try { + File outputDir; + if (Case.isCaseOpen()) { + outputDir = new File(Case.getCurrentCase().getOutputDirectory()); + if (outputDir.exists() == false) { + NotifyDescriptor d + = new NotifyDescriptor.Message(NbBundle.getMessage(this.getClass(), + "OpenOutputFolder.error1", outputDir.getAbsolutePath()), + NotifyDescriptor.ERROR_MESSAGE); + DialogDisplayer.getDefault().notify(d); + } else { + Desktop.getDesktop().open(outputDir); + } + } else { + JOptionPane.showMessageDialog(null, NbBundle.getMessage(this.getClass(), "OpenOutputFolder.noCaseOpen")); + } + } catch (IOException ex) { + logger.log(Level.WARNING, NbBundle.getMessage(this.getClass(), "OpenOutputFolder.CouldNotOpenOutputFolder"), ex); //NON-NLS + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardChooseDataSourcePanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardChooseDataSourcePanel.java index 384f8f9cc3..88762ba96a 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardChooseDataSourcePanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardChooseDataSourcePanel.java @@ -20,12 +20,10 @@ package org.sleuthkit.autopsy.casemodule; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; -import java.io.File; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.logging.Level; - import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; import javax.swing.event.ChangeEvent; @@ -33,7 +31,8 @@ import javax.swing.event.ChangeListener; import org.openide.WizardDescriptor; import org.openide.util.HelpCtx; import org.openide.util.Lookup; -import org.sleuthkit.autopsy.coreutils.ModuleSettings; +import org.openide.windows.WindowManager; +import java.awt.Cursor; /** * The "Add Image" wizard panel1 handling the logic of selecting image file(s) @@ -70,6 +69,7 @@ class AddImageWizardChooseDataSourcePanel implements WizardDescriptor.Panel sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,13 +25,13 @@ import java.awt.Window; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.UUID; import javax.swing.JButton; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import javax.swing.event.ChangeListener; import org.openide.WizardDescriptor; import org.openide.util.HelpCtx; -import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.Content; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; @@ -47,7 +47,7 @@ import org.sleuthkit.autopsy.ingest.IngestManager; */ class AddImageWizardIngestConfigPanel implements WizardDescriptor.Panel { - private IngestJobSettingsPanel ingestJobSettingsPanel; + private final IngestJobSettingsPanel ingestJobSettingsPanel; /** * The visual component that displays this panel. If you need to access the @@ -220,13 +220,14 @@ class AddImageWizardIngestConfigPanel implements WizardDescriptor.Panel { + Case.getCurrentCase().notifyAddingDataSource(dataSourceId); + }).start(); DataSourceProcessorCallback cbObj = new DataSourceProcessorCallback() { @Override public void doneEDT(DataSourceProcessorCallback.DataSourceProcessorResult result, List errList, List contents) { - dataSourceProcessorDone(result, errList, contents); + dataSourceProcessorDone(dataSourceId, result, errList, contents); } }; @@ -253,7 +257,10 @@ class AddImageWizardIngestConfigPanel implements WizardDescriptor.Panel { + Case.getCurrentCase().notifyFailedAddingDataSource(dataSourceId); + }).start(); dsProcessor.cancel(); } @@ -261,8 +268,7 @@ class AddImageWizardIngestConfigPanel implements WizardDescriptor.Panel errList, List contents) { - + private void dataSourceProcessorDone(UUID dataSourceId, DataSourceProcessorCallback.DataSourceProcessorResult result, List errList, List contents) { // disable the cleanup task cleanupTask.disable(); @@ -301,10 +307,13 @@ class AddImageWizardIngestConfigPanel implements WizardDescriptor.Panel { + if (!newContents.isEmpty()) { + Case.getCurrentCase().notifyDataSourceAdded(newContents.get(0), dataSourceId); + } else { + Case.getCurrentCase().notifyFailedAddingDataSource(dataSourceId); + } + }).start(); // Start ingest if we can progressPanel.setStateStarted(); diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties index ec12ed5a4d..c9a86ea72f 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties @@ -107,13 +107,13 @@ AddImageWizardIngestConfigVisual.getName.text=Configure Ingest Modules AddImageWizardIterator.stepXofN=Step {0} of {1} AddLocalFilesTask.localFileAdd.progress.text=Adding\: {0}/{1} Case.getCurCase.exception.noneOpen=Cannot get the current case; there is no case open\! -Case.moduleErr=Module Error -Case.changeCase.errListenToCaseUpdates.msg=A module caused an error listening to Case updates. See log to determine which module. Some data could be incomplete. Case.create.exception.msg=Error creating a case\: {0} in dir {1} +Case.databaseConnectionInfo.error.msg=Error accessing case database connection info Case.open.exception.blankCase.msg=Case name is blank. 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.checkFile.msg=Check that you selected the correct case file (usually with {0} extension) +Case.open.exception.multiUserCaseNotEnabled=Cannot open a multi-user case if multi-user cases are not enabled. See Tools, Options, Multi-user. Case.open.exception.gen.msg=Error opening the case Case.checkImgExist.confDlg.doesntExist.msg={0} has detected that one of the images associated with \n\ this case are missing. Would you like to search for them now?\n\ @@ -135,7 +135,11 @@ Case.createCaseDir.exception.existCantRW=Cannot create case dir, already exists Case.createCaseDir.exception.cantCreate=Cannot create case dir\: {0} Case.createCaseDir.exception.cantCreateCaseDir=Could not create case directory\: {0} Case.createCaseDir.exception.cantCreateModDir=Could not create modules output directory\: {0} +Case.createCaseDir.exception.cantCreateReportsDir=Could not create reports output directory\: {0} Case.createCaseDir.exception.gen=Could not create case directory\: {0} +Case.CollaborationSetup.FailNotify.ErrMsg=Failed to connect to any other nodes that may be collaborating on this case. +Case.CollaborationSetup.FailNotify.Title=Connection Failure +Case.GetCaseTypeGivenPath.Failure=Unable to get case type CaseDeleteAction.closeConfMsg.text=Are you sure want to close and delete this case? \n\ Case Name\: {0}\n\ Case Directory\: {1} @@ -145,10 +149,11 @@ Close the folder and file and try again or you can delete the case manually. CaseDeleteAction.msgDlg.fileInUse.title=Error\: Folder In Use CaseDeleteAction.msgDlg.caseDelete.msg=Case {0} has been deleted. CaseOpenAction.autFilter.title={0} Case File ( {1}) -CaseOpenAction.msgDlg.fileNotExist.msg=Error\: File does not exist. -CaseOpenAction.msgDlg.fileNotExist.title=Error -CaseOpenAction.msgDlg.cantOpenCase.msg=Error\: could not open the case in folder {0}\: {1} -CaseOpenAction.msgDlg.cantOpenCase.title=Error +CaseOpenAction.msgDlg.cantOpenCase.title=Error Opening Case +CaseCreateAction.msgDlg.cantCreateCase.msg=Cannot create case +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 @@ -183,13 +188,18 @@ MissingImageDialog.allDesc.text=All Supported Types MissingImageDialog.display.title=Search for Missing Image MissingImageDialog.confDlg.noFileSel.msg=No image file has been selected, are you sure you\nwould like to exit without finding the image. MissingImageDialog.confDlg.noFileSel.title=Missing Image +MissingImageDialog.ErrorSettingImage=Error setting image path. Please try again. NewCaseVisualPanel1.getName.text=Case Info NewCaseVisualPanel1.caseDirBrowse.selectButton.text=Select +NewCaseVisualPanel1.badCredentials.text=Bad multi-user settings (see Tools, Options, Multi-user) or services are down. +NewCaseVisualPanel1.MultiUserDisabled.text=Multi-user cases not enabled. See Tools, Options, Multi-user. NewCaseVisualPanel2.getName.text=Additional Information NewCaseWizardAction.closeCurCase.confMsg.msg=Do you want to save and close this case and proceed with the new case creation? NewCaseWizardAction.closeCurCase.confMsg.title=Warning\: Closing the Current Case NewCaseWizardAction.newCase.windowTitle.text=New Case Information NewCaseWizardAction.getName.text=New Case Wizard +NewCaseWizardAction.databaseProblem1.text=Cannot open database. Cancelling case creation. +NewCaseWizardAction.databaseProblem2.text=Error NewCaseWizardPanel1.validate.errMsg.invalidSymbols=The Case Name cannot contain any of the following symbols\: \\ / \: * ? " < > | NewCaseWizardPanel1.validate.errMsg.dirExists=Case directory ''{0}'' already exists. NewCaseWizardPanel1.validate.confMsg.createDir.msg=The base directory "{0}" does not exist. \n\n\ @@ -221,5 +231,33 @@ Detail\: \n\ Cannot open a non-Autopsy config file (at {1}). XMLCaseManagement.open.msgDlg.notAutCase.title=Error AddImageWizardIngestConfigPanel.CANCEL_BUTTON.text=Cancel +ImageFilePanel.errorLabel.text=Error Label +DataSourceOnCDriveError.text=Warning: Path to multi-user data source is on \"C:\" drive +NewCaseVisualPanel1.CaseFolderOnCDriveError.text=Warning: Path to multi-user case folder is on \"C:\" drive +LocalFilesPanel.errorLabel.text=Error Label +CollaborationMonitor.addingDataSourceStatus.msg={0} adding data source +CollaborationMonitor.analyzingDataSourceStatus.msg={0} analyzing {1} +MissingImageDialog.lbWarning.text= +MissingImageDialog.lbWarning.toolTipText= +SingleUserCaseImporter.AlreadyMultiUser=Case is already multi-user! +SingleUserCaseImporter.BadCaseSourceFolder=Case source folder does not exist! +SingleUserCaseImporter.BadImageSourceFolder=Image source folder does not exist! +SingleUserCaseImporter.BadDatabaseFileName=Database file does not exist! +SingleUserCaseImporter.NonUniqueOutputFolder=Output folder not unique. Skipping +SingleUserCaseImporter.NonUniqueDatabaseName=Database name not unique. Skipping. +SingleUserCaseImporter.PotentiallyNonUniqueDatabaseName=Unclear if database name unique. Moving ahead. +SingleUserCaseImporter.ImportedAsMultiUser=\nThis case was imported as a multi-user collaborative case on +SingleUserCaseImporter.UnableToCopySourceImages=Unable to copy source images +SingleUserCaseImporter.DeletingCase=Deleting original case folder +SingleUserCaseImporter.CanNotOpenDatabase=Unable to open database +SingleUserCaseImporter.WillImport=Will import: +SingleUserCaseImporter.WillNotImport=Will not import: +SingleUserCaseImporter.None=None +SingleUserCaseImporter.ContinueWithImport=Continue with import? +SingleUserCaseImporter.Cancelled=Cancelled +NewCaseVisualPanel1.caseParentDirWarningLabel.text=Case directory warning label +NewCaseVisualPanel1.multiUserCaseRadioButton.text=Multi-user +NewCaseVisualPanel1.singleUserCaseRadioButton.text=Single-user +NewCaseVisualPanel1.multiUserSettingsWarningLabel.text=Multi-user settings warning label Case.deleteReports.deleteFromDiskException.log.msg=Unable to delete the report from the disk. Case.deleteReports.deleteFromDiskException.msg=Unable to delete the report {0} from the disk.\nYou may manually delete it from {1} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle_ja.properties b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle_ja.properties index 3eb6853e11..5dff497d16 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle_ja.properties +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle_ja.properties @@ -102,8 +102,6 @@ AddImageWizardIngestConfigVisual.getName.text=\u30a4\u30f3\u30b8\u30a7\u30b9\u30 AddImageWizardIterator.stepXofN=\u30b9\u30c6\u30c3\u30d7{0}\uff0f{1} AddLocalFilesTask.localFileAdd.progress.text=\u8ffd\u52a0\u4e2d\uff1a{0}/{1} Case.getCurCase.exception.noneOpen=\u73fe\u5728\u306e\u30b1\u30fc\u30b9\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\uff1b\u958b\u3044\u3066\u3044\u308b\u30b1\u30fc\u30b9\u304c\u3042\u308a\u307e\u305b\u3093\uff01 -Case.moduleErr=\u30e2\u30b8\u30e5\u30fc\u30eb\u30a8\u30e9\u30fc -Case.changeCase.errListenToCaseUpdates.msg=\u30b1\u30fc\u30b9\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u3092\u78ba\u8a8d\u4e2d\u306b\u30e2\u30b8\u30e5\u30fc\u30eb\u304c\u30a8\u30e9\u30fc\u3092\u8d77\u3053\u3057\u307e\u3057\u305f\u3002\u3069\u306e\u30e2\u30b8\u30e5\u30fc\u30eb\u304b\u30ed\u30b0\u3067\u78ba\u8a8d\u3057\u3066\u4e0b\u3055\u3044\u3002\u4e00\u90e8\u306e\u30c7\u30fc\u30bf\u304c\u4e0d\u5b8c\u5168\u304b\u3082\u3057\u308c\u307e\u305b\u3093\u3002 Case.create.exception.msg=\u30b1\u30fc\u30b9\u4f5c\u6210\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\uff1a\u30c7\u30a3\u30ec\u30af\u30c8\u30ea{1}\u306e{0} Case.open.exception.blankCase.msg=\u30b1\u30fc\u30b9\u540d\u304c\u7a7a\u767d\u3067\u3059\u3002 Case.open.msgDlg.updated.msg=\u30b1\u30fc\u30b9\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u30b9\u30ad\u30fc\u30de\u3092\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u3057\u307e\u3057\u305f\u3002\n\ @@ -217,3 +215,6 @@ XMLCaseManagement.open.msgDlg.notAutCase.title=\u30a8\u30e9\u30fc ImageFilePanel.noFatOrphansCheckbox.text=FAT\u30d5\u30a1\u30a4\u30eb\u30b7\u30b9\u30c6\u30e0\u306e\u30aa\u30fc\u30d5\u30a1\u30f3\u30d5\u30a1\u30a4\u30eb\u306f\u7121\u8996 LocalDiskPanel.noFatOrphansCheckbox.text=FAT\u30d5\u30a1\u30a4\u30eb\u30b7\u30b9\u30c6\u30e0\u306e\u30aa\u30fc\u30d5\u30a1\u30f3\u30d5\u30a1\u30a4\u30eb\u306f\u7121\u8996 AddImageWizardIngestConfigPanel.CANCEL_BUTTON.text=\u30ad\u30e3\u30f3\u30bb\u30eb +ImageFilePanel.errorLabel.text=\u30a8\u30e9\u30fc\u30e9\u30d9\u30eb +LocalFilesPanel.errorLabel.text=\u30a8\u30e9\u30fc\u30e9\u30d9\u30eb +NewCaseVisualPanel1.caseParentDirWarningLabel.text=\u30a8\u30e9\u30fc\u30e9\u30d9\u30eb diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java index 04b88a496b..de01134bcd 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java @@ -19,7 +19,6 @@ package org.sleuthkit.autopsy.casemodule; import java.awt.Frame; -import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.BufferedInputStream; @@ -27,9 +26,11 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.nio.file.InvalidPathException; +import java.nio.file.Path; import java.nio.file.Paths; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.util.Date; import java.util.Collection; import java.util.GregorianCalendar; import java.util.HashMap; @@ -38,26 +39,39 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.TimeZone; +import java.util.UUID; import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import org.apache.commons.io.FileUtils; -import org.openide.util.Lookup; import org.openide.util.NbBundle; import org.openide.util.actions.CallableSystemAction; -import org.openide.util.actions.SystemAction; import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.casemodule.CaseMetadata.CaseMetadataException; +import org.sleuthkit.autopsy.casemodule.events.AddingDataSourceEvent; +import org.sleuthkit.autopsy.casemodule.events.AddingDataSourceFailedEvent; +import org.sleuthkit.autopsy.casemodule.events.DataSourceAddedEvent; +import org.sleuthkit.autopsy.casemodule.events.ReportAddedEvent; import org.sleuthkit.autopsy.casemodule.services.Services; +import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.corecomponentinterfaces.CoreComponentControl; import org.sleuthkit.autopsy.coreutils.FileUtil; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.PlatformUtil; import org.sleuthkit.autopsy.coreutils.Version; -import org.sleuthkit.autopsy.events.BlackBoardArtifactTagAddedEvent; -import org.sleuthkit.autopsy.events.BlackBoardArtifactTagDeletedEvent; -import org.sleuthkit.autopsy.events.ContentTagAddedEvent; -import org.sleuthkit.autopsy.events.ContentTagDeletedEvent; +import org.sleuthkit.autopsy.coreutils.NetworkUtils; +import org.sleuthkit.autopsy.events.AutopsyEvent; +import org.sleuthkit.autopsy.events.AutopsyEventException; +import org.sleuthkit.autopsy.events.AutopsyEventPublisher; +import org.sleuthkit.autopsy.casemodule.events.BlackBoardArtifactTagAddedEvent; +import org.sleuthkit.autopsy.casemodule.events.BlackBoardArtifactTagDeletedEvent; +import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; +import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; +import org.sleuthkit.autopsy.core.RuntimeProperties; +import org.sleuthkit.autopsy.core.UserPreferencesException; import org.sleuthkit.datamodel.BlackboardArtifactTag; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.ContentTag; @@ -73,19 +87,26 @@ import org.sleuthkit.datamodel.TskException; * open at a time. Use getCurrentCase() to retrieve the object for the current * case. */ -@SuppressWarnings("deprecation") // TODO: Remove this when ErrorObserver is replaced. public class Case implements SleuthkitCase.ErrorObserver { private static final String autopsyVer = Version.getVersion(); // current version of autopsy. Change it when the version is changed + private static final String EVENT_CHANNEL_NAME = "%s-Case-Events"; private static String appName = null; + volatile private IntervalErrorReportData tskErrorReporter = null; + private static final int MIN_SECONDS_BETWEEN_ERROR_REPORTS = 60; // No less than 60 seconds between warnings for errors + private static final int MAX_SANITIZED_NAME_LENGTH = 47; /** * Name for the property that determines whether to show the dialog at * startup */ public static final String propStartup = "LBL_StartupDialog"; //NON-NLS - // pcs is initialized in CaseListener constructor - private static final PropertyChangeSupport pcs = new PropertyChangeSupport(Case.class); + + /** + * The event publisher is static so that subscribers only have to subscribe + * once to receive events for all cases. + */ + private static final AutopsyEventPublisher eventPublisher = new AutopsyEventPublisher(); /** * Events that the case module will fire. Event listeners can get the event @@ -95,12 +116,9 @@ public class Case implements SleuthkitCase.ErrorObserver { /** * Property name that indicates the name of the current case has - * changed. When a case is opened, "old name" is empty string and "new - * name" is the name. When a case is closed, "old name" is the case name - * and "new name" is empty string. When a case is renamed, "old name" - * has the original name and "new name" has the new name. + * changed. The old value is the old case name, the new value is the new + * case name. */ - // @@@ BC: I propose that this is no longer called for case open/close. NAME, /** * Property name that indicates the number of the current case has @@ -115,6 +133,24 @@ public class Case implements SleuthkitCase.ErrorObserver { * no examiner set. */ EXAMINER, + /** + * Property name used for a property change event that indicates a new + * data source (image, local/logical file or local disk) is being added + * to the current case. The old and new values of the + * PropertyChangeEvent are null - cast the PropertyChangeEvent to + * org.sleuthkit.autopsy.casemodule.events.AddingDataSourceEvent to + * access event data. + */ + ADDING_DATA_SOURCE, + /** + * Property name used for a property change event that indicates a + * failure adding a new data source (image, local/logical file or local + * disk) to the current case. The old and new values of the + * PropertyChangeEvent are null - cast the PropertyChangeEvent to + * org.sleuthkit.autopsy.casemodule.events.AddingDataSourceFailedEvent + * to access event data. + */ + ADDING_DATA_SOURCE_FAILED, /** * Property name that indicates a new data source (image, disk or local * file) has been added to the current case. The new value is the @@ -156,7 +192,9 @@ public class Case implements SleuthkitCase.ErrorObserver { BLACKBOARD_ARTIFACT_TAG_ADDED, /** * Property name for the event when a new BlackBoardArtifactTag is - * deleted. The new value is empty, the old value is the deleted tag + * deleted. The new value is empty, the old value is a + * {@link BlackBoardArtifactTagDeletedEvent.DeletedBlackboardArtifactTagInfo} + * object with info about the deleted tag. */ BLACKBOARD_ARTIFACT_TAG_DELETED, /** @@ -166,11 +204,49 @@ public class Case implements SleuthkitCase.ErrorObserver { CONTENT_TAG_ADDED, /** * Property name for the event when a new ContentTag is deleted. The new - * value is empty, the old value is the deleted tag + * value is empty, the old value is a + * {@link ContentTagDeletedEvent.DeletedContentTagInfo} object with info + * about the deleted tag. */ CONTENT_TAG_DELETED; }; + /** + * This enum describes the type of case, either single-user (standalone) or + * multi-user (using PostgreSql) + */ + public enum CaseType { + + SINGLE_USER_CASE("Single-user case"), + MULTI_USER_CASE("Multi-user case"); + + private final String caseType; + + private CaseType(String s) { + caseType = s; + } + + public boolean equalsName(String otherType) { + return (otherType == null) ? false : caseType.equals(otherType); + } + + public static CaseType fromString(String typeName) { + if (typeName != null) { + for (CaseType c : CaseType.values()) { + if (typeName.equalsIgnoreCase(c.caseType)) { + return c; + } + } + } + return null; + } + + @Override + public String toString() { + return caseType; + } + }; + private String name; private String number; private String examiner; @@ -179,35 +255,37 @@ public class Case implements SleuthkitCase.ErrorObserver { private final SleuthkitCase db; // Track the current case (only set with changeCase() method) private static Case currentCase = null; + private final CaseType caseType; private final Services services; private static final Logger logger = Logger.getLogger(Case.class.getName()); static final String CASE_EXTENSION = "aut"; //NON-NLS static final String CASE_DOT_EXTENSION = "." + CASE_EXTENSION; + private final static String CACHE_FOLDER = "Cache"; //NON-NLS + private final static String EXPORT_FOLDER = "Export"; //NON-NLS + private final static String LOG_FOLDER = "Log"; //NON-NLS + final static String MODULE_FOLDER = "ModuleOutput"; //NON-NLS + private final static String REPORTS_FOLDER = "Reports"; //NON-NLS + private final static String TEMP_FOLDER = "Temp"; //NON-NLS // we cache if the case has data in it yet since a few places ask for it and we dont' need to keep going to DB private boolean hasData = false; + private CollaborationMonitor collaborationMonitor; + /** * Constructor for the Case class */ - private Case(String name, String number, String examiner, String configFilePath, XMLCaseManagement xmlcm, SleuthkitCase db) { + private Case(String name, String number, String examiner, String configFilePath, XMLCaseManagement xmlcm, SleuthkitCase db, CaseType type) { this.name = name; this.number = number; this.examiner = examiner; this.configFilePath = configFilePath; this.xmlcm = xmlcm; + this.caseType = type; this.db = db; this.services = new Services(db); } - /** - * Does initialization that would leak a reference to this if done in the - * constructor. - */ - private void init() { - db.addErrorObserver(this); - } - /** * Gets the currently opened case, if there is one. * @@ -240,72 +318,93 @@ public class Case implements SleuthkitCase.ErrorObserver { * */ private static void changeCase(Case newCase) { - // close the existing case Case oldCase = Case.currentCase; Case.currentCase = null; if (oldCase != null) { - doCaseChange(null); //closes windows, etc - - try { - pcs.firePropertyChange(Events.CURRENT_CASE.toString(), oldCase, null); - } catch (Exception e) { - logger.log(Level.SEVERE, "Case listener threw exception", e); //NON-NLS - MessageNotifyUtil.Notify.show(NbBundle.getMessage(Case.class, "Case.moduleErr"), - NbBundle.getMessage(Case.class, - "Case.changeCase.errListenToCaseUpdates.msg"), - MessageNotifyUtil.MessageType.ERROR); + doCaseChange(null); //closes windows, etc + if (null != oldCase.tskErrorReporter) { + oldCase.tskErrorReporter.shutdown(); // stop listening for TSK errors for the old case + oldCase.tskErrorReporter = null; } - doCaseNameChange(""); - - try { - pcs.firePropertyChange(Events.NAME.toString(), oldCase.name, ""); - } catch (Exception e) { - logger.log(Level.SEVERE, "Case listener threw exception", e); //NON-NLS - MessageNotifyUtil.Notify.show(NbBundle.getMessage(Case.class, "Case.moduleErr"), - NbBundle.getMessage(Case.class, - "Case.changeCase.errListenToCaseUpdates.msg"), - MessageNotifyUtil.MessageType.ERROR); + eventPublisher.publishLocally(new AutopsyEvent(Events.CURRENT_CASE.toString(), oldCase, null)); + if (CaseType.MULTI_USER_CASE == oldCase.getCaseType()) { + if (null != oldCase.collaborationMonitor) { + oldCase.collaborationMonitor.shutdown(); + } + eventPublisher.closeRemoteEventChannel(); } } if (newCase != null) { currentCase = newCase; - Logger.setLogDirectory(currentCase.getLogDirectoryPath()); - - try { - pcs.firePropertyChange(Events.CURRENT_CASE.toString(), null, currentCase); - } catch (Exception e) { - logger.log(Level.SEVERE, "Case listener threw exception", e); //NON-NLS - MessageNotifyUtil.Notify.show(NbBundle.getMessage(Case.class, "Case.moduleErr"), - NbBundle.getMessage(Case.class, - "Case.changeCase.errListenToCaseUpdates.msg"), - MessageNotifyUtil.MessageType.ERROR); + // sanity check + if (null != currentCase.tskErrorReporter) { + currentCase.tskErrorReporter.shutdown(); } + // start listening for TSK errors for the new case + currentCase.tskErrorReporter = new IntervalErrorReportData(currentCase, MIN_SECONDS_BETWEEN_ERROR_REPORTS, + NbBundle.getMessage(Case.class, "IntervalErrorReport.ErrorText")); doCaseChange(currentCase); - - try { - pcs.firePropertyChange(Events.NAME.toString(), "", currentCase.name); - } catch (Exception e) { - logger.log(Level.SEVERE, "Case threw exception", e); //NON-NLS - MessageNotifyUtil.Notify.show(NbBundle.getMessage(Case.class, "Case.moduleErr"), - NbBundle.getMessage(Case.class, - "Case.changeCase.errListenToCaseUpdates.msg"), - MessageNotifyUtil.MessageType.ERROR); + SwingUtilities.invokeLater(() -> { + RecentCases.getInstance().addRecentCase(currentCase.name, currentCase.configFilePath); // update the recent cases + }); + if (CaseType.MULTI_USER_CASE == newCase.getCaseType()) { + try { + /** + * Use the text index name as the remote event channel name + * prefix since it is unique, the same as the case database + * name for a multiuser case, and is readily available + * through the Case.getTextIndexName() API. + */ + eventPublisher.openRemoteEventChannel(String.format(EVENT_CHANNEL_NAME, newCase.getTextIndexName())); + currentCase.collaborationMonitor = new CollaborationMonitor(); + } catch (AutopsyEventException | CollaborationMonitor.CollaborationMonitorException ex) { + logger.log(Level.SEVERE, "Failed to setup for collaboration", ex); + MessageNotifyUtil.Notify.error(NbBundle.getMessage(Case.class, "Case.CollaborationSetup.FailNotify.Title"), NbBundle.getMessage(Case.class, "Case.CollaborationSetup.FailNotify.ErrMsg")); + } } - doCaseNameChange(currentCase.name); + eventPublisher.publishLocally(new AutopsyEvent(Events.CURRENT_CASE.toString(), null, currentCase)); - RecentCases.getInstance().addRecentCase(currentCase.name, currentCase.configFilePath); // update the recent cases } else { Logger.setLogDirectory(PlatformUtil.getLogDirectory()); } } + + @Override + public void receiveError(String context, String errorMessage) { + /* NOTE: We are accessing tskErrorReporter from two different threads. + * This is ok as long as we only read the value of tskErrorReporter + * because tskErrorReporter is declared as volatile. + */ + if (null != tskErrorReporter) { + tskErrorReporter.addProblems(context, errorMessage); + } + } AddImageProcess makeAddImageProcess(String timezone, boolean processUnallocSpace, boolean noFatOrphans) { return this.db.makeAddImageProcess(timezone, processUnallocSpace, noFatOrphans); } + /** + * Creates a new case (create the XML config file and database). Overload + * for API consistency, defaults to a single-user case. + * + * @param caseDir The directory to store case data in. Will be created if + * it doesn't already exist. If it exists, it should have + * all of the needed sub dirs that createCaseDirectory() + * will create. + * @param caseName the name of case + * @param caseNumber the case number + * @param examiner the examiner for this case + * + * @throws org.sleuthkit.autopsy.casemodule.CaseActionException + */ + public static void create(String caseDir, String caseName, String caseNumber, String examiner) throws CaseActionException { + create(caseDir, caseName, caseNumber, examiner, CaseType.SINGLE_USER_CASE); + } + /** * Creates a new case (create the XML config file and database) * @@ -316,98 +415,219 @@ public class Case implements SleuthkitCase.ErrorObserver { * @param caseName the name of case * @param caseNumber the case number * @param examiner the examiner for this case + * @param caseType the type of case, single-user or multi-user */ - public static void create(String caseDir, String caseName, String caseNumber, String examiner) throws CaseActionException { + public static void create(String caseDir, String caseName, String caseNumber, String examiner, CaseType caseType) throws CaseActionException { logger.log(Level.INFO, "Creating new case.\ncaseDir: {0}\ncaseName: {1}", new Object[]{caseDir, caseName}); //NON-NLS // create case directory if it doesn't already exist. if (new File(caseDir).exists() == false) { - Case.createCaseDirectory(caseDir); + Case.createCaseDirectory(caseDir, caseType); } String configFilePath = caseDir + File.separator + caseName + CASE_DOT_EXTENSION; XMLCaseManagement xmlcm = new XMLCaseManagement(); - xmlcm.create(caseDir, caseName, examiner, caseNumber); // create a new XML config file + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss"); + Date date = new Date(); + String santizedCaseName = sanitizeCaseName(caseName); + String indexName = santizedCaseName + "_" + dateFormat.format(date); + String dbName = null; + + // figure out the database name and index name for text extraction + if (caseType == CaseType.SINGLE_USER_CASE) { + dbName = caseDir + File.separator + "autopsy.db"; //NON-NLS + } else if (caseType == CaseType.MULTI_USER_CASE) { + dbName = indexName; + } + + xmlcm.create(caseDir, caseName, examiner, caseNumber, caseType, dbName, indexName); // create a new XML config file xmlcm.writeFile(); - String dbPath = caseDir + File.separator + "autopsy.db"; //NON-NLS SleuthkitCase db = null; try { - db = SleuthkitCase.newCase(dbPath); + if (caseType == CaseType.SINGLE_USER_CASE) { + db = SleuthkitCase.newCase(dbName); + } else if (caseType == CaseType.MULTI_USER_CASE) { + db = SleuthkitCase.newCase(dbName, UserPreferences.getDatabaseConnectionInfo(), caseDir); + } } catch (TskCoreException ex) { logger.log(Level.SEVERE, "Error creating a case: " + caseName + " in dir " + caseDir, ex); //NON-NLS throw new CaseActionException( NbBundle.getMessage(Case.class, "Case.create.exception.msg", caseName, caseDir), ex); + } catch (UserPreferencesException ex) { + logger.log(Level.SEVERE, "Error accessing case database connection info", ex); //NON-NLS + throw new CaseActionException( + NbBundle.getMessage(Case.class, "Case.databaseConnectionInfo.error.msg"), ex); } /** * Two-stage initialization to avoid leaking reference to "this" in * constructor. */ - Case newCase = new Case(caseName, caseNumber, examiner, configFilePath, xmlcm, db); - newCase.init(); - + Case newCase = new Case(caseName, caseNumber, examiner, configFilePath, xmlcm, db, caseType); changeCase(newCase); } /** - * Opens the existing case (open the XML config file) + * Sanitize the case name for PostgreSQL database, Solr cores, and ActiveMQ + * topics. Makes it plain-vanilla enough that each item should be able to + * use it. * - * @param configFilePath the path of the configuration file that's opened + * Sanitize the PostgreSQL/Solr core, and ActiveMQ name by excluding: + * Control characters Non-ASCII characters Various others shown below + * + * Solr: + * http://stackoverflow.com/questions/29977519/what-makes-an-invalid-core-name + * may not be / \ : + * + * ActiveMQ: + * http://activemq.2283324.n4.nabble.com/What-are-limitations-restrictions-on-destination-name-td4664141.html + * may not be ? + * + * PostgreSQL: + * http://www.postgresql.org/docs/9.4/static/sql-syntax-lexical.html 63 + * chars max, must start with a-z or _ following chars can be letters _ or + * digits + * + * SQLite: Uses autopsy.db for the database name follows Windows naming + * convention + * + * @param caseName The name of the case as typed in by the user + * + * @return the sanitized case name to use for Database, Solr, and ActiveMQ + */ + public static String sanitizeCaseName(String caseName) { + + String result; + + // Remove all non-ASCII characters + result = caseName.replaceAll("[^\\p{ASCII}]", "_"); + + // Remove all control characters + result = result.replaceAll("[\\p{Cntrl}]", "_"); + + // Remove / \ : ? space ' " + result = result.replaceAll("[ /?:'\"\\\\]", "_"); + + // Make it all lowercase + result = result.toLowerCase(); + + // Must start with letter or underscore for PostgreSQL. If not, prepend an underscore. + if (result.length() > 0 && !(Character.isLetter(result.codePointAt(0))) && !(result.codePointAt(0) == '_')) { + result = "_" + result; + } + + // Chop to 63-16=47 left (63 max for PostgreSQL, taking 16 for the date _20151225_123456) + if (result.length() > MAX_SANITIZED_NAME_LENGTH) { + result = result.substring(0, MAX_SANITIZED_NAME_LENGTH); + } + + if (result.isEmpty()) { + result = "case"; + } + + return result; + } + + /** + * Opens an existing case. + * + * @param caseMetadataFilePath The path of the case metadata file for the + * case to be opened. * * @throws CaseActionException */ - public static void open(String configFilePath) throws CaseActionException { - logger.log(Level.INFO, "Opening case.\nconfigFilePath: {0}", configFilePath); //NON-NLS + /** + * TODO: Deprecate this and throw a more general exception. + */ + public static void open(String caseMetadataFilePath) throws CaseActionException { + if (!caseMetadataFilePath.endsWith(CASE_DOT_EXTENSION)) { + throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.open.exception.checkFile.msg", CASE_DOT_EXTENSION)); + } + + logger.log(Level.INFO, "Opening case, case metadata file path: {0}", caseMetadataFilePath); //NON-NLS try { - XMLCaseManagement xmlcm = new XMLCaseManagement(); + /** + * Get the case metadata from the file. + */ + CaseMetadata metadata = new CaseMetadata(Paths.get(caseMetadataFilePath)); + String caseName = metadata.getCaseName(); + String caseNumber = metadata.getCaseNumber(); + String examiner = metadata.getExaminer(); + CaseType caseType = metadata.getCaseType(); + String caseDir = metadata.getCaseDirectory(); - xmlcm.open(configFilePath); // open and load the config file to the document handler in the XML class - xmlcm.writeFile(); // write any changes to the config file - - String caseName = xmlcm.getCaseName(); - String caseNumber = xmlcm.getCaseNumber(); - String examiner = xmlcm.getCaseExaminer(); - // if the caseName is "", case / config file can't be opened - if (caseName.equals("")) { - throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.open.exception.blankCase.msg")); + /** + * Open the case database. + */ + SleuthkitCase db; + if (caseType == CaseType.SINGLE_USER_CASE) { + String dbPath = Paths.get(caseDir, "autopsy.db").toString(); //NON-NLS + db = SleuthkitCase.openCase(dbPath); + } else { + if (!UserPreferences.getIsMultiUserModeEnabled()) { + throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.open.exception.multiUserCaseNotEnabled")); + } + try { + db = SleuthkitCase.openCase(metadata.getCaseDatabaseName(), UserPreferences.getDatabaseConnectionInfo(), caseDir); + } catch (UserPreferencesException ex) { + logger.log(Level.SEVERE, "Error accessing case database connection info", ex); //NON-NLS + throw new CaseActionException( + NbBundle.getMessage(Case.class, "Case.databaseConnectionInfo.error.msg"), ex); + } } - String caseDir = xmlcm.getCaseDirectory(); - String dbPath = caseDir + File.separator + "autopsy.db"; //NON-NLS - SleuthkitCase db = SleuthkitCase.openCase(dbPath); - if (null != db.getBackupDatabasePath()) { - JOptionPane.showMessageDialog(null, - NbBundle.getMessage(Case.class, "Case.open.msgDlg.updated.msg", - db.getBackupDatabasePath()), - NbBundle.getMessage(Case.class, "Case.open.msgDlg.updated.title"), - JOptionPane.INFORMATION_MESSAGE); - } + /** + * Do things that require a UI. + */ + if (RuntimeProperties.coreComponentsAreActive()) { + /** + * If the case database was upgraded for a new schema, notify + * the user. + */ + if (null != db.getBackupDatabasePath()) { + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog(null, + NbBundle.getMessage(Case.class, "Case.open.msgDlg.updated.msg", + db.getBackupDatabasePath()), + NbBundle.getMessage(Case.class, "Case.open.msgDlg.updated.title"), + JOptionPane.INFORMATION_MESSAGE); + }); + } - checkImagesExist(db); + /** + * TODO: This currently has no value if it there is no user to + * interact with a fid missing images dialog. + */ + checkImagesExist(db); + } /** * Two-stage initialization to avoid leaking reference to "this" in - * constructor. + * constructor. TODO: Remove use of obsolete XMLCaseManagement + * class. */ - Case openedCase = new Case(caseName, caseNumber, examiner, configFilePath, xmlcm, db); - openedCase.init(); - + XMLCaseManagement xmlcm = new XMLCaseManagement(); + xmlcm.open(caseMetadataFilePath); + Case openedCase = new Case(caseName, caseNumber, examiner, caseMetadataFilePath, xmlcm, db, caseType); changeCase(openedCase); - } catch (Exception ex) { - logger.log(Level.SEVERE, "Error opening the case: ", ex); //NON-NLS - // close the previous case if there's any - CaseCloseAction closeCase = SystemAction.get(CaseCloseAction.class); - closeCase.actionPerformed(null); - if (!configFilePath.endsWith(CASE_DOT_EXTENSION)) { - throw new CaseActionException( - NbBundle.getMessage(Case.class, "Case.open.exception.checkFile.msg", CASE_DOT_EXTENSION), ex); - } else { - throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.open.exception.gen.msg") + ". " + ex.getMessage(), ex); + } catch (CaseMetadataException | TskCoreException ex) { + /** + * Clean-up the case if it was actually opened. TODO: Do this + * better. + */ + try { + Case badCase = Case.getCurrentCase(); + badCase.closeCase(); + } catch (CaseActionException | IllegalStateException unused) { + // Already logged. } + + throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.open.exception.gen.msg") + ". " + ex.getMessage(), ex); } } @@ -440,7 +660,7 @@ public class Case implements SleuthkitCase.ErrorObserver { int ret = JOptionPane.showConfirmDialog(null, NbBundle.getMessage(Case.class, "Case.checkImgExist.confDlg.doesntExist.msg", - appName, path), + getAppName(), path), NbBundle.getMessage(Case.class, "Case.checkImgExist.confDlg.doesntExist.title"), JOptionPane.YES_NO_OPTION); @@ -457,110 +677,127 @@ public class Case implements SleuthkitCase.ErrorObserver { } /** - * Adds the image to the current case after it has been added to the DB + * Adds the image to the current case after it has been added to the DB. * Sends out event and reopens windows if needed. * * @param imgPaths the paths of the image that being added * @param imgId the ID of the image that being added * @param timeZone the timeZone of the image where it's added + * + * @deprecated As of release 4.0, replaced by {@link #notifyAddingDataSource(java.util.UUID) and + * {@link #notifyDataSourceAdded(org.sleuthkit.datamodel.Content, java.util.UUID) and + * {@link #notifyFailedAddingDataSource(java.util.UUID)} */ @Deprecated public Image addImage(String imgPath, long imgId, String timeZone) throws CaseActionException { - logger.log(Level.INFO, "Adding image to Case. imgPath: {0} ID: {1} TimeZone: {2}", new Object[]{imgPath, imgId, timeZone}); //NON-NLS - try { - Image newImage = db.getImageById(imgId); - - try { - pcs.firePropertyChange(Events.DATA_SOURCE_ADDED.toString(), null, newImage); // the new value is the instance of the image - } catch (Exception e) { - logger.log(Level.SEVERE, "Case listener threw exception", e); //NON-NLS - MessageNotifyUtil.Notify.show(NbBundle.getMessage(this.getClass(), "Case.moduleErr"), - NbBundle.getMessage(this.getClass(), - "Case.changeCase.errListenToCaseUpdates.msg"), - MessageNotifyUtil.MessageType.ERROR); - } - CoreComponentControl.openCoreWindows(); - return newImage; + Image newDataSource = db.getImageById(imgId); + notifyDataSourceAdded(newDataSource, UUID.randomUUID()); + return newDataSource; } catch (Exception ex) { throw new CaseActionException(NbBundle.getMessage(this.getClass(), "Case.addImg.exception.msg"), ex); } } /** - * Finishes adding new local data source to the case Sends out event and - * reopens windows if needed. + * Finishes adding new local data source to the case. Sends out event and + * reopens windows if needed. * * @param newDataSource new data source added + * + * @deprecated As of release 4.0, replaced by {@link #notifyAddingDataSource(java.util.UUID) and + * {@link #notifyDataSourceAdded(org.sleuthkit.datamodel.Content, java.util.UUID) and + * {@link #notifyFailedAddingDataSource(java.util.UUID)} */ @Deprecated void addLocalDataSource(Content newDataSource) { - - notifyNewDataSource(newDataSource); + notifyDataSourceAdded(newDataSource, UUID.randomUUID()); } /** - * Notifies the UI that a new data source has been added. + * Notifies case event subscribers (property change listeners) that a data + * source is being added to the case database. * + * This should not be called from the event dispatch thread (EDT) * - * @param newDataSource new data source added + * @param dataSourceId A unique identifier for the data source. This UUID + * should be used to call notifyNewDataSource() after + * the data source is added. */ - void notifyNewDataSource(Content newDataSource) { - notifyPropertyChangeEvent(new PropertyChangeEvent(Case.class, Events.DATA_SOURCE_ADDED.toString(), null, newDataSource)); - CoreComponentControl.openCoreWindows(); + public void notifyAddingDataSource(UUID dataSourceId) { + eventPublisher.publish(new AddingDataSourceEvent(dataSourceId)); + } + + /** + * Notifies case event subscribers (property change listeners) that a data + * source failed to be added to the case database. + * + * This should not be called from the event dispatch thread (EDT) + * + * @param dataSourceId A unique identifier for the data source. + */ + public void notifyFailedAddingDataSource(UUID dataSourceId) { + eventPublisher.publish(new AddingDataSourceFailedEvent(dataSourceId)); + } + + /** + * Notifies case event subscribers (property change listeners) that a data + * source is being added to the case database. + * + * This should not be called from the event dispatch thread (EDT) + * + * @param newDataSource New data source added. + * @param dataSourceId A unique identifier for the data source. Should be + * the same UUID used to call + * notifyAddingNewDataSource() when the process of + * adding the data source began. + */ + public void notifyDataSourceAdded(Content newDataSource, UUID dataSourceId) { + eventPublisher.publish(new DataSourceAddedEvent(newDataSource, dataSourceId)); } /** * Notifies the UI that a new ContentTag has been added. * + * This should not be called from the event dispatch thread (EDT) + * * @param newTag new ContentTag added */ public void notifyContentTagAdded(ContentTag newTag) { - notifyPropertyChangeEvent(new ContentTagAddedEvent(newTag)); + eventPublisher.publish(new ContentTagAddedEvent(newTag)); } /** * Notifies the UI that a ContentTag has been deleted. * + * This should not be called from the event dispatch thread (EDT) + * * @param deletedTag ContentTag deleted */ public void notifyContentTagDeleted(ContentTag deletedTag) { - notifyPropertyChangeEvent(new ContentTagDeletedEvent(deletedTag)); + eventPublisher.publish(new ContentTagDeletedEvent(deletedTag)); } /** * Notifies the UI that a new BlackboardArtifactTag has been added. * + * This should not be called from the event dispatch thread (EDT) + * * @param newTag new BlackboardArtifactTag added */ public void notifyBlackBoardArtifactTagAdded(BlackboardArtifactTag newTag) { - notifyPropertyChangeEvent(new BlackBoardArtifactTagAddedEvent(newTag)); + eventPublisher.publish(new BlackBoardArtifactTagAddedEvent(newTag)); } /** - * Notifies the UI that a BlackboardArtifactTag has been. + * Notifies the UI that a BlackboardArtifactTag has been deleted. + * + * This should not be called from the event dispatch thread (EDT) * * @param deletedTag BlackboardArtifactTag deleted */ public void notifyBlackBoardArtifactTagDeleted(BlackboardArtifactTag deletedTag) { - notifyPropertyChangeEvent(new BlackBoardArtifactTagDeletedEvent(deletedTag)); - } - - /** - * Notifies the UI about a Case level event. - * - * @param propertyChangeEvent the event to distribute - */ - private void notifyPropertyChangeEvent(final PropertyChangeEvent propertyChangeEvent) { - try { - pcs.firePropertyChange(propertyChangeEvent); - } catch (Exception e) { - logger.log(Level.SEVERE, "Case threw exception", e); //NON-NLS - MessageNotifyUtil.Notify.show(NbBundle.getMessage(this.getClass(), "Case.moduleErr"), - NbBundle.getMessage(this.getClass(), - "Case.changeCase.errListenToCaseUpdates.msg"), - MessageNotifyUtil.MessageType.ERROR); - } + eventPublisher.publish(new BlackBoardArtifactTagDeletedEvent(deletedTag)); } /** @@ -585,7 +822,6 @@ public class Case implements SleuthkitCase.ErrorObserver { */ public void closeCase() throws CaseActionException { changeCase(null); - try { services.close(); this.xmlcm.close(); // close the xmlcm @@ -626,6 +862,8 @@ public class Case implements SleuthkitCase.ErrorObserver { /** * Updates the case name. * + * This should not be called from the EDT. + * * @param oldCaseName the old case name that wants to be updated * @param oldPath the old path that wants to be updated * @param newCaseName the new case name @@ -635,18 +873,15 @@ public class Case implements SleuthkitCase.ErrorObserver { try { xmlcm.setCaseName(newCaseName); // set the case name = newCaseName; // change the local value - RecentCases.getInstance().updateRecentCase(oldCaseName, oldPath, newCaseName, newPath); // update the recent case - try { - pcs.firePropertyChange(Events.NAME.toString(), oldCaseName, newCaseName); - } catch (Exception e) { - logger.log(Level.SEVERE, "Case listener threw exception", e); //NON-NLS - MessageNotifyUtil.Notify.show(NbBundle.getMessage(this.getClass(), "Case.moduleErr"), - NbBundle.getMessage(this.getClass(), - "Case.changeCase.errListenToCaseUpdates.msg"), - MessageNotifyUtil.MessageType.ERROR); - } - doCaseNameChange(newCaseName); - + eventPublisher.publish(new AutopsyEvent(Events.NAME.toString(), oldCaseName, newCaseName)); + SwingUtilities.invokeLater(() -> { + try { + RecentCases.getInstance().updateRecentCase(oldCaseName, oldPath, newCaseName, newPath); // update the recent case + updateMainWindowTitle(newCaseName); + } catch (Exception e) { + Logger.getLogger(CasePropertiesForm.class.getName()).log(Level.WARNING, "Error: problem updating case name.", e); //NON-NLS + } + }); } catch (Exception e) { throw new CaseActionException(NbBundle.getMessage(this.getClass(), "Case.updateCaseName.exception.msg"), e); } @@ -655,6 +890,8 @@ public class Case implements SleuthkitCase.ErrorObserver { /** * Updates the case examiner * + * This should not be called from the EDT. + * * @param oldExaminer the old examiner * @param newExaminer the new examiner */ @@ -662,15 +899,7 @@ public class Case implements SleuthkitCase.ErrorObserver { try { xmlcm.setCaseExaminer(newExaminer); // set the examiner examiner = newExaminer; - try { - pcs.firePropertyChange(Events.EXAMINER.toString(), oldExaminer, newExaminer); - } catch (Exception e) { - logger.log(Level.SEVERE, "Case listener threw exception", e); //NON-NLS - MessageNotifyUtil.Notify.show(NbBundle.getMessage(this.getClass(), "Case.moduleErr"), - NbBundle.getMessage(this.getClass(), - "Case.changeCase.errListenToCaseUpdates.msg"), - MessageNotifyUtil.MessageType.ERROR); - } + eventPublisher.publish(new AutopsyEvent(Events.EXAMINER.toString(), oldExaminer, newExaminer)); } catch (Exception e) { throw new CaseActionException(NbBundle.getMessage(this.getClass(), "Case.updateExaminer.exception.msg"), e); } @@ -679,6 +908,8 @@ public class Case implements SleuthkitCase.ErrorObserver { /** * Updates the case number * + * This should not be called from the EDT. + * * @param oldCaseNumber the old case number * @param newCaseNumber the new case number */ @@ -686,16 +917,7 @@ public class Case implements SleuthkitCase.ErrorObserver { try { xmlcm.setCaseNumber(newCaseNumber); // set the case number number = newCaseNumber; - - try { - pcs.firePropertyChange(Events.NUMBER.toString(), oldCaseNumber, newCaseNumber); - } catch (Exception e) { - logger.log(Level.SEVERE, "Case listener threw exception", e); //NON-NLS - MessageNotifyUtil.Notify.show(NbBundle.getMessage(this.getClass(), "Case.moduleErr"), - NbBundle.getMessage(this.getClass(), - "Case.changeCase.errListenToCaseUpdates.msg"), - MessageNotifyUtil.MessageType.ERROR); - } + eventPublisher.publish(new AutopsyEvent(Events.NUMBER.toString(), oldCaseNumber, newCaseNumber)); } catch (Exception e) { throw new CaseActionException(NbBundle.getMessage(this.getClass(), "Case.updateCaseNum.exception.msg"), e); } @@ -790,55 +1012,192 @@ public class Case implements SleuthkitCase.ErrorObserver { } /** - * Gets the full path to the temp directory of this case + * Get the case type. + * + * @return + */ + public CaseType getCaseType() { + return this.caseType; + } + + /** + * Gets the full path to the temp directory of this case. Will create it if + * it does not already exist. * * @return tempDirectoryPath */ public String getTempDirectory() { - if (xmlcm == null) { - return ""; - } else { - return xmlcm.getTempDir(); - } + return getDirectory(TEMP_FOLDER); } /** - * Gets the full path to the cache directory of this case + * Gets the full path to the cache directory of this case. Will create it if + * it does not already exist. * * @return cacheDirectoryPath */ public String getCacheDirectory() { - if (xmlcm == null) { - return ""; - } else { - return xmlcm.getCacheDir(); - } + return getDirectory(CACHE_FOLDER); } /** - * Gets the full path to the export directory of this case + * Gets the full path to the export directory of this case. Will create it + * if it does not already exist. * - * @return export DirectoryPath + * @return exportDirectoryPath */ public String getExportDirectory() { - if (xmlcm == null) { - return ""; - } else { - return xmlcm.getExportDir(); - } + return getDirectory(EXPORT_FOLDER); } /** - * Gets the full path to the log directory for this case. + * Gets the full path to the log directory of this case. Will create it if + * it does not already exist. * - * @return The log directory path. + * @return logDirectoryPath */ public String getLogDirectoryPath() { - if (xmlcm == null) { - return ""; - } else { - return xmlcm.getLogDir(); + return getDirectory(LOG_FOLDER); + } + + /** + * Get the reports directory path where modules should save their reports. + * Will create it if it does not already exist. + * + * @return absolute path to the report output directory + */ + public String getReportDirectory() { + return getDirectory(REPORTS_FOLDER); + } + + /** + * Get module output directory path where modules should save their + * permanent data. + * + * @return absolute path to the module output directory + */ + public String getModuleDirectory() { + return getDirectory(MODULE_FOLDER); + } + + /** + * Get the output directory path where modules should save their permanent + * data. If single-user case, the directory is a subdirectory of the case + * directory. If multi-user case, the directory is a subdirectory of + * HostName, which is a subdirectory of the case directory. + * + * @return the path to the host output directory + */ + public String getOutputDirectory() { + return getHostDirectory(); + } + + /** + * Get the specified directory path, create it if it does not already exist. + * + * @return absolute path to the directory + */ + private String getDirectory(String input) { + File theDirectory = new File(getHostDirectory() + File.separator + input); + if (!theDirectory.exists()) { // Create it if it doesn't exist already. + theDirectory.mkdirs(); } + return theDirectory.toString(); + } + + /** + * Get relative (with respect to case dir) module output directory path + * where modules should save their permanent data. The directory is a + * subdirectory of this case dir. + * + * @return relative path to the module output dir + */ + public String getModuleOutputDirectoryRelativePath() { + Path thePath; + if (getCaseType() == CaseType.MULTI_USER_CASE) { + thePath = Paths.get(NetworkUtils.getLocalHostName(), MODULE_FOLDER); + } else { + thePath = Paths.get(MODULE_FOLDER); + } + // Do not autocreate this relative path. It will have already been + // created when the case was made. + return thePath.toString(); + } + + /** + * Get the host output directory path where modules should save their + * permanent data. If single-user case, the directory is a subdirectory of + * the case directory. If multi-user case, the directory is a subdirectory + * of the hostName, which is a subdirectory of the case directory. + * + * @return the path to the host output directory + */ + private String getHostDirectory() { + String caseDirectory = getCaseDirectory(); + Path hostPath; + if (caseType == CaseType.MULTI_USER_CASE) { + hostPath = Paths.get(caseDirectory, NetworkUtils.getLocalHostName()); + } else { + hostPath = Paths.get(caseDirectory); + } + if (!hostPath.toFile().exists()) { + hostPath.toFile().mkdirs(); + } + return hostPath.toString(); + } + + /** + * Get module output directory path where modules should save their + * permanent data. + * + * @return absolute path to the module output directory + * + * @deprecated Use getModuleDirectory() instead. + */ + @Deprecated + public String getModulesOutputDirAbsPath() { + return getModuleDirectory(); + } + + /** + * Get relative (with respect to case dir) module output directory path + * where modules should save their permanent data. The directory is a + * subdirectory of this case dir. + * + * @return relative path to the module output dir + * + * @deprecated Use getModuleOutputDirectoryRelativePath() instead + */ + @Deprecated + public static String getModulesOutputDirRelPath() { + return "ModuleOutput"; //NON-NLS + } + + /** + * Gets a PropertyChangeSupport object. The PropertyChangeSupport object + * returned is not used by instances of this class and does not have any + * PropertyChangeListeners. + * + * @return A new PropertyChangeSupport object. + * + * @deprecated Do not use. + */ + @Deprecated + public static PropertyChangeSupport getPropertyChangeSupport() { + return new PropertyChangeSupport(Case.class); + } + + /** + * Get the data model Content objects in the root of this case's hierarchy. + * + * @return a list of the root objects + * + * @throws org.sleuthkit.datamodel.TskCoreException + */ + public List getDataSources() throws TskCoreException { + List list = db.getRootObjects(); + hasData = (list.size() > 0); + return list; } /** @@ -855,46 +1214,16 @@ public class Case implements SleuthkitCase.ErrorObserver { } /** - * Get absolute module output directory path where modules should save their - * permanent data The directory is a subdirectory of this case dir. + * Get the name of the index where extracted text is stored for the case. * - * @return absolute path to the module output dir + * @return Index name. */ - public String getModulesOutputDirAbsPath() { - return this.getCaseDirectory() + File.separator + getModulesOutputDirRelPath(); - } - - /** - * Get relative (with respect to case dir) module output directory path - * where modules should save their permanent data The directory is a - * subdirectory of this case dir. - * - * @return relative path to the module output dir - */ - public static String getModulesOutputDirRelPath() { - return "ModuleOutput"; //NON-NLS - } - - /** - * get the PropertyChangeSupport of this class - * - * @return PropertyChangeSupport - */ - public static PropertyChangeSupport getPropertyChangeSupport() { - return pcs; - } - - /** - * Get the data model Content objects in the root of this case's hierarchy. - * - * @return a list of the root objects - * - * @throws org.sleuthkit.datamodel.TskCoreException - */ - public List getDataSources() throws TskCoreException { - List list = db.getRootObjects(); - hasData = (list.size() > 0); - return list; + public String getTextIndexName() { + if (xmlcm == null) { + return ""; + } else { + return xmlcm.getTextIndexName(); + } } /** @@ -918,12 +1247,74 @@ public class Case implements SleuthkitCase.ErrorObserver { return timezones; } + /** + * Adds a subscriber to all case events from this Autopsy node and other + * Autopsy nodes. To subscribe to only specific events, use one of the + * overloads of addEventSubscriber(). + * + * @param listener The subscriber to add. + */ public static synchronized void addPropertyChangeListener(PropertyChangeListener listener) { - pcs.addPropertyChangeListener(listener); + addEventSubscriber(Stream.of(Events.values()) + .map(Events::toString) + .collect(Collectors.toSet()), listener); } + /** + * Removes a subscriber from all case events from this Autopsy node and + * other Autopsy nodes. To remove a subscription to only specific events, + * use one of the overloads of removeEventSubscriber(). + * + * @param listener The subscriber to add. + */ public static synchronized void removePropertyChangeListener(PropertyChangeListener listener) { - pcs.removePropertyChangeListener(listener); + removeEventSubscriber(Stream.of(Events.values()) + .map(Events::toString) + .collect(Collectors.toSet()), listener); + } + + /** + * Adds a subscriber to events from this Autopsy node and other Autopsy + * nodes. + * + * @param eventNames The events the subscriber is interested in. + * @param subscriber The subscriber to add. + */ + public static void addEventSubscriber(Set eventNames, PropertyChangeListener subscriber) { + eventPublisher.addSubscriber(eventNames, subscriber); + } + + /** + * Adds a subscriber to events from this Autopsy node and other Autopsy + * nodes. + * + * @param eventName The event the subscriber is interested in. + * @param subscriber The subscriber to add. + */ + public static void addEventSubscriber(String eventName, PropertyChangeListener subscriber) { + eventPublisher.addSubscriber(eventName, subscriber); + } + + /** + * Adds a subscriber to events from this Autopsy node and other Autopsy + * nodes. + * + * @param eventName The event the subscriber is no longer interested in. + * @param subscriber The subscriber to add. + */ + public static void removeEventSubscriber(String eventName, PropertyChangeListener subscriber) { + eventPublisher.removeSubscriber(eventName, subscriber); + } + + /** + * Removes a subscriber to events from this Autopsy node and other Autopsy + * nodes. + * + * @param eventNames The event the subscriber is no longer interested in. + * @param subscriber The subscriber to add. + */ + public static void removeEventSubscriber(Set eventNames, PropertyChangeListener subscriber) { + eventPublisher.removeSubscriber(eventNames, subscriber); } /** @@ -1015,10 +1406,6 @@ public class Case implements SleuthkitCase.ErrorObserver { return result; } - /* - * The methods below are used to manage the case directories (creating, - * checking, deleting, etc) - */ /** * to create the case directory * @@ -1028,19 +1415,20 @@ public class Case implements SleuthkitCase.ErrorObserver { * @throws CaseActionException throw if could not create the case dir * @Deprecated */ + @Deprecated static void createCaseDirectory(String caseDir, String caseName) throws CaseActionException { - createCaseDirectory(caseDir); - + createCaseDirectory(caseDir, CaseType.SINGLE_USER_CASE); } /** * Create the case directory and its needed subfolders. * - * @param caseDir Path to the case directory (typically base + case name) + * @param caseDir Path to the case directory (typically base + case name) + * @param caseType The type of case, single-user or multi-user * * @throws CaseActionException throw if could not create the case dir */ - static void createCaseDirectory(String caseDir) throws CaseActionException { + static void createCaseDirectory(String caseDir, CaseType caseType) throws CaseActionException { File caseDirF = new File(caseDir); if (caseDirF.exists()) { @@ -1061,17 +1449,22 @@ public class Case implements SleuthkitCase.ErrorObserver { } // create the folders inside the case directory - result = result && (new File(caseDir + File.separator + XMLCaseManagement.EXPORT_FOLDER_RELPATH)).mkdir() - && (new File(caseDir + File.separator + XMLCaseManagement.LOG_FOLDER_RELPATH)).mkdir() - && (new File(caseDir + File.separator + XMLCaseManagement.TEMP_FOLDER_RELPATH)).mkdir() - && (new File(caseDir + File.separator + XMLCaseManagement.CACHE_FOLDER_RELPATH)).mkdir(); + String hostClause = ""; + + if (caseType == CaseType.MULTI_USER_CASE) { + hostClause = File.separator + NetworkUtils.getLocalHostName(); + } + result = result && (new File(caseDir + hostClause + File.separator + EXPORT_FOLDER)).mkdirs() + && (new File(caseDir + hostClause + File.separator + LOG_FOLDER)).mkdirs() + && (new File(caseDir + hostClause + File.separator + TEMP_FOLDER)).mkdirs() + && (new File(caseDir + hostClause + File.separator + CACHE_FOLDER)).mkdirs(); if (result == false) { throw new CaseActionException( NbBundle.getMessage(Case.class, "Case.createCaseDir.exception.cantCreateCaseDir", caseDir)); } - final String modulesOutDir = caseDir + File.separator + getModulesOutputDirRelPath(); + final String modulesOutDir = caseDir + hostClause + File.separator + MODULE_FOLDER; result = new File(modulesOutDir).mkdir(); if (result == false) { throw new CaseActionException( @@ -1079,6 +1472,14 @@ public class Case implements SleuthkitCase.ErrorObserver { modulesOutDir)); } + final String reportsOutDir = caseDir + hostClause + File.separator + REPORTS_FOLDER; + result = new File(reportsOutDir).mkdir(); + if (result == false) { + throw new CaseActionException( + NbBundle.getMessage(Case.class, "Case.createCaseDir.exception.cantCreateReportsDir", + modulesOutDir)); + } + } catch (Exception e) { throw new CaseActionException( NbBundle.getMessage(Case.class, "Case.createCaseDir.exception.gen", caseDir), e); @@ -1104,20 +1505,6 @@ public class Case implements SleuthkitCase.ErrorObserver { StartupWindowProvider.getInstance().open(); } - /** - * Call if there are no images in the case. Displays a dialog offering to - * add one. - */ - private static void runAddImageAction() { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - final AddImageAction action = Lookup.getDefault().lookup(AddImageAction.class); - action.actionPerformed(null); - } - }); - } - /** * Checks if a String is a valid case name * @@ -1153,7 +1540,7 @@ public class Case implements SleuthkitCase.ErrorObserver { * @param openedCase */ private static void checkSubFolders(Case openedCase) { - String modulesOutputDir = openedCase.getModulesOutputDirAbsPath(); + String modulesOutputDir = openedCase.getModuleDirectory(); File modulesOutputDirF = new File(modulesOutputDir); if (!modulesOutputDirF.exists()) { logger.log(Level.INFO, "Creating modules output dir for the case."); //NON-NLS @@ -1177,34 +1564,63 @@ public class Case implements SleuthkitCase.ErrorObserver { Case.clearTempFolder(); checkSubFolders(toChangeTo); - // enable these menus - CallableSystemAction.get(AddImageAction.class).setEnabled(true); - CallableSystemAction.get(CaseCloseAction.class).setEnabled(true); - CallableSystemAction.get(CasePropertiesAction.class).setEnabled(true); - CallableSystemAction.get(CaseDeleteAction.class).setEnabled(true); // Delete Case menu + if (RuntimeProperties.coreComponentsAreActive()) { + // enable these menus + SwingUtilities.invokeLater(() -> { + CallableSystemAction.get(AddImageAction.class).setEnabled(true); + CallableSystemAction.get(CaseCloseAction.class).setEnabled(true); + CallableSystemAction.get(CasePropertiesAction.class).setEnabled(true); + CallableSystemAction.get(CaseDeleteAction.class).setEnabled(true); // Delete Case menu + }); - if (toChangeTo.hasData()) { - // open all top components - CoreComponentControl.openCoreWindows(); - } else { - // close all top components - CoreComponentControl.closeCoreWindows(); + if (toChangeTo.hasData()) { + // open all top components + SwingUtilities.invokeLater(() -> { + CoreComponentControl.openCoreWindows(); + }); + } else { + // close all top components + SwingUtilities.invokeLater(() -> { + CoreComponentControl.closeCoreWindows(); + }); + } } - } else { // case is closed - // close all top components first - CoreComponentControl.closeCoreWindows(); - // disable these menus - CallableSystemAction.get(AddImageAction.class).setEnabled(false); // Add Image menu - CallableSystemAction.get(CaseCloseAction.class).setEnabled(false); // Case Close menu - CallableSystemAction.get(CasePropertiesAction.class).setEnabled(false); // Case Properties menu - CallableSystemAction.get(CaseDeleteAction.class).setEnabled(false); // Delete Case menu + if (RuntimeProperties.coreComponentsAreActive()) { + SwingUtilities.invokeLater(() -> { + updateMainWindowTitle(currentCase.name); + }); + } else { + SwingUtilities.invokeLater(() -> { + Frame f = WindowManager.getDefault().getMainWindow(); + f.setTitle(Case.getAppName()); // set the window name to just application name + }); + } + + } else { // case is closed + if (RuntimeProperties.coreComponentsAreActive()) { + + SwingUtilities.invokeLater(() -> { + // close all top components first + CoreComponentControl.closeCoreWindows(); + + // disable these menus + CallableSystemAction.get(AddImageAction.class).setEnabled(false); // Add Image menu + CallableSystemAction.get(CaseCloseAction.class).setEnabled(false); // Case Close menu + CallableSystemAction.get(CasePropertiesAction.class).setEnabled(false); // Case Properties menu + CallableSystemAction.get(CaseDeleteAction.class).setEnabled(false); // Delete Case menu + }); + } //clear pending notifications - MessageNotifyUtil.Notify.clear(); + SwingUtilities.invokeLater(() -> { + MessageNotifyUtil.Notify.clear(); + }); - Frame f = WindowManager.getDefault().getMainWindow(); - f.setTitle(Case.getAppName()); // set the window name to just application name + SwingUtilities.invokeLater(() -> { + Frame f = WindowManager.getDefault().getMainWindow(); + f.setTitle(Case.getAppName()); // set the window name to just application name + }); //try to force gc to happen System.gc(); @@ -1217,7 +1633,7 @@ public class Case implements SleuthkitCase.ErrorObserver { } //case name change helper - private static void doCaseNameChange(String newCaseName) { + private static void updateMainWindowTitle(String newCaseName) { // update case name if (!newCaseName.equals("")) { Frame f = WindowManager.getDefault().getMainWindow(); @@ -1225,29 +1641,13 @@ public class Case implements SleuthkitCase.ErrorObserver { } } - //delete image helper - private void doDeleteImage() { - // no more image left in this case - if (currentCase.hasData()) { - // close all top components - CoreComponentControl.closeCoreWindows(); - } - } - - @Override - public void receiveError(String context, String errorMessage) { - MessageNotifyUtil.Notify.error(context, errorMessage); - } - /** * Adds a report to the case. * - * @param localPath The path of the report file, must be in the case - * directory or one of its subdirectories. - * @param sourceModuleName The name of the module that created the report. - * @param reportName The report name, may be empty. - * - * @return A Report data transfer object (DTO) for the new row. + * @param localPath The path of the report file, must be in the case + * directory or one of its subdirectories. + * @param srcModuleName The name of the module that created the report. + * @param reportName The report name, may be empty. * * @throws TskCoreException */ @@ -1260,12 +1660,7 @@ public class Case implements SleuthkitCase.ErrorObserver { throw new TskCoreException(errorMsg, ex); } Report report = this.db.addReport(normalizedLocalPath, srcModuleName, reportName); - try { - Case.pcs.firePropertyChange(Events.REPORT_ADDED.toString(), null, report); - } catch (Exception ex) { - String errorMessage = String.format("A Case %s listener threw an exception", Events.REPORT_ADDED.toString()); //NON-NLS - logger.log(Level.SEVERE, errorMessage, ex); - } + eventPublisher.publish(new ReportAddedEvent(report)); } public List getAllReports() throws TskCoreException { @@ -1306,13 +1701,7 @@ public class Case implements SleuthkitCase.ErrorObserver { } } - // fire property change event. - try { - Case.pcs.firePropertyChange(Events.REPORT_DELETED.toString(), null, null); - } catch (Exception ex) { - String errorMessage = String.format("A Case %s listener threw an exception", Events.REPORT_DELETED.toString()); //NON-NLS - logger.log(Level.SEVERE, errorMessage, ex); - } + eventPublisher.publish(new AutopsyEvent(Events.REPORT_DELETED.toString(), null, null)); } } @@ -1331,4 +1720,5 @@ public class Case implements SleuthkitCase.ErrorObserver { } return hasData; } + } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseCloseAction.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseCloseAction.java index 5f953c7bf9..1fc7c79575 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseCloseAction.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseCloseAction.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2014 Basis Technology Corp. + * Copyright 2011-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,15 +19,11 @@ package org.sleuthkit.autopsy.casemodule; import java.awt.Component; -import java.awt.EventQueue; import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.util.logging.Level; -import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import javax.swing.Action; import javax.swing.ImageIcon; import javax.swing.JButton; +import javax.swing.SwingWorker; import org.openide.util.HelpCtx; import org.openide.util.NbBundle; import org.openide.util.actions.CallableSystemAction; @@ -37,7 +33,7 @@ import org.openide.util.actions.Presenter; * The action to close the current Case. This class should be disabled on * creation and it will be enabled on new case creation or case opened. */ -final class CaseCloseAction extends CallableSystemAction implements Presenter.Toolbar { +public final class CaseCloseAction extends CallableSystemAction implements Presenter.Toolbar { JButton toolbarButton = new JButton(); @@ -49,13 +45,7 @@ final class CaseCloseAction extends CallableSystemAction implements Presenter.To putValue(Action.NAME, NbBundle.getMessage(CaseCloseAction.class, "CTL_CaseCloseAct")); // put the action Name // set action of the toolbar button - toolbarButton.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - CaseCloseAction.this.actionPerformed(e); - } - }); + toolbarButton.addActionListener(CaseCloseAction.this::actionPerformed); this.setEnabled(false); } @@ -71,23 +61,24 @@ final class CaseCloseAction extends CallableSystemAction implements Presenter.To return; } - Case result = Case.getCurrentCase(); + new SwingWorker() { - if (!MessageNotifyUtil.Message.confirm("Are you sure you want to close current case?")) { - return; - } - - try { - result.closeCase(); - EventQueue.invokeLater(new Runnable() { - @Override - public void run() { - StartupWindowProvider.getInstance().open(); + @Override + protected Void doInBackground() throws Exception { + try { + Case result = Case.getCurrentCase(); + result.closeCase(); + } catch (CaseActionException | IllegalStateException unused) { + // Already logged. } - }); - } catch (Exception ex) { - Logger.getLogger(CaseCloseAction.class.getName()).log(Level.WARNING, "Error closing case.", ex); //NON-NLS - } + return null; + } + + @Override + protected void done() { + StartupWindowProvider.getInstance().open(); + } + }.execute(); } /** diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java new file mode 100644 index 0000000000..b2bbb54397 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java @@ -0,0 +1,136 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-2015 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; + +import java.nio.file.Path; + +/** + * Provides access to case metadata. + */ +public final class CaseMetadata { + + /** + * Exception thrown by the CaseMetadata class when there is a problem + * accessing the metadata for a case. + */ + public final static class CaseMetadataException extends Exception { + + private CaseMetadataException(String message) { + super(message); + } + + private CaseMetadataException(String message, Throwable cause) { + super(message, cause); + } + } + + private final Case.CaseType caseType; + private final String caseName; + private final String caseNumber; + private final String examiner; + private final String caseDirectory; + private final String caseDatabaseName; + + /** + * Constructs an object that provides access to case metadata. + * + * @param metadataFilePath + */ + public CaseMetadata(Path metadataFilePath) throws CaseMetadataException { + try { + // NOTE: This class will eventually replace XMLCaseManagement. + // This constructor should parse all of the metadata. In the future, + // case metadata may be moved into the case database. + XMLCaseManagement metadata = new XMLCaseManagement(); + metadata.open(metadataFilePath.toString()); + caseType = metadata.getCaseType(); + caseName = metadata.getCaseName(); + if (caseName.isEmpty()) { + throw new CaseMetadataException("Case name missing"); + } + caseNumber = metadata.getCaseNumber(); + examiner = metadata.getCaseExaminer(); + caseDirectory = metadata.getCaseDirectory(); + if (caseDirectory.isEmpty()) { + throw new CaseMetadataException("Case directory missing"); + } + caseDatabaseName = metadata.getDatabaseName(); + if (Case.CaseType.MULTI_USER_CASE == caseType && caseDatabaseName.isEmpty()) { + throw new CaseMetadataException("Case database name missing"); + } + } catch (CaseActionException ex) { + throw new CaseMetadataException(ex.getLocalizedMessage(), ex); + } + } + + /** + * Gets the case type. + * + * @return The case type. + */ + public Case.CaseType getCaseType() { + return this.caseType; + } + + /** + * Gets the case name. + * + * @return The case name. + */ + public String getCaseName() { + return caseName; + } + + /** + * Gets the case number. + * + * @return The case number, may be empty. + */ + public String getCaseNumber() { + return caseNumber; + } + + /** + * Gets the examiner. + * + * @return The examiner, may be empty. + */ + public String getExaminer() { + return examiner; + } + + /** + * Gets the case directory. + * + * @return The case directory. + */ + public String getCaseDirectory() { + return caseDirectory; + } + + /** + * Gets the case database name. + * + * @return The case database name, will be empty for a single-user case. + */ + public String getCaseDatabaseName() { + return caseDatabaseName; + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseNewAction.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseNewAction.java index 125063f4be..4e8243d3ba 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseNewAction.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseNewAction.java @@ -19,6 +19,9 @@ package org.sleuthkit.autopsy.casemodule; import java.awt.event.ActionEvent; +import org.openide.util.HelpCtx; +import org.openide.util.NbBundle; +import org.openide.util.actions.CallableSystemAction; import org.openide.util.actions.SystemAction; import org.openide.util.lookup.ServiceProvider; @@ -28,7 +31,7 @@ import org.openide.util.lookup.ServiceProvider; * @author jantonius */ @ServiceProvider(service = CaseNewActionInterface.class) -public final class CaseNewAction implements CaseNewActionInterface { +public final class CaseNewAction extends CallableSystemAction implements CaseNewActionInterface { private NewCaseWizardAction wizard = SystemAction.get(NewCaseWizardAction.class); @@ -41,4 +44,18 @@ public final class CaseNewAction implements CaseNewActionInterface { public void actionPerformed(ActionEvent e) { wizard.performAction(); } + + @Override + public void performAction() { + } + + @Override + public String getName() { + return NbBundle.getMessage(CaseNewAction.class, "CTL_CaseNewAction"); + } + + @Override + public HelpCtx getHelpCtx() { + return HelpCtx.DEFAULT_HELP; + } } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java index f6bb0070dd..7cea2ef29e 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2014 Basis Technology Corp. + * Copyright 2011-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,97 +18,87 @@ */ package org.sleuthkit.autopsy.casemodule; -import java.awt.Component; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; -import java.util.logging.Level; import javax.swing.JFileChooser; import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; import javax.swing.filechooser.FileFilter; import javax.swing.filechooser.FileNameExtensionFilter; import org.openide.util.NbBundle; import org.openide.util.lookup.ServiceProvider; +import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.coreutils.ModuleSettings; -import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Version; /** - * The action to open a existing case. This class is always enabled. + * An action that opens an existing case. */ @ServiceProvider(service = CaseOpenAction.class) public final class CaseOpenAction implements ActionListener { - private static final Logger logger = Logger.getLogger(CaseOpenAction.class.getName()); private static final String PROP_BASECASE = "LBL_BaseCase_PATH"; //NON-NLS - private final JFileChooser fc = new JFileChooser(); - private FileFilter autFilter; + private final JFileChooser fileChooser = new JFileChooser(); + private final FileFilter caseMetadataFileFilter; /** - * The constructor + * Constructs an action that opens an existing case. */ public CaseOpenAction() { - autFilter = new FileNameExtensionFilter( - NbBundle.getMessage(CaseOpenAction.class, "CaseOpenAction.autFilter.title", Version.getName(), - Case.CASE_DOT_EXTENSION), - Case.CASE_EXTENSION); - fc.setDragEnabled(false); - fc.setFileSelectionMode(JFileChooser.FILES_ONLY); - fc.setMultiSelectionEnabled(false); - fc.setFileFilter(autFilter); - try { - if (ModuleSettings.getConfigSetting(ModuleSettings.MAIN_SETTINGS, PROP_BASECASE) != null) { - fc.setCurrentDirectory(new File(ModuleSettings.getConfigSetting("Case", PROP_BASECASE))); //NON-NLS - } - } catch (Exception e) { + caseMetadataFileFilter = new FileNameExtensionFilter(NbBundle.getMessage(CaseOpenAction.class, "CaseOpenAction.autFilter.title", Version.getName(), Case.CASE_DOT_EXTENSION), Case.CASE_EXTENSION); + fileChooser.setDragEnabled(false); + fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); + fileChooser.setMultiSelectionEnabled(false); + fileChooser.setFileFilter(caseMetadataFileFilter); + if (null != ModuleSettings.getConfigSetting(ModuleSettings.MAIN_SETTINGS, PROP_BASECASE)) { + fileChooser.setCurrentDirectory(new File(ModuleSettings.getConfigSetting("Case", PROP_BASECASE))); //NON-NLS } } /** - * Pop-up the File Chooser to open the existing case (.aut file) + * Pops up a file chooser to allow the user to select a case meta data file + * (.aut file) and attempts to open the case described by the file. * - * @param e the action event + * @param e The action event. */ @Override public void actionPerformed(ActionEvent e) { - int retval = fc.showOpenDialog((Component) e.getSource()); - + /** + * Pop up a file chooser to allow the user to select a case meta data + * file (.aut file) + */ + int retval = fileChooser.showOpenDialog(WindowManager.getDefault().getMainWindow()); if (retval == JFileChooser.APPROVE_OPTION) { - String path = fc.getSelectedFile().getPath(); - String dirPath = fc.getSelectedFile().getParent(); - ModuleSettings.setConfigSetting(ModuleSettings.MAIN_SETTINGS, PROP_BASECASE, dirPath.substring(0, dirPath.lastIndexOf(File.separator))); - // check if the file exists - if (!new File(path).exists()) { - JOptionPane.showMessageDialog(null, - NbBundle.getMessage(this.getClass(), - "CaseOpenAction.msgDlg.fileNotExist.msg"), - NbBundle.getMessage(this.getClass(), - "CaseOpenAction.msgDlg.fileNotExist.title"), - JOptionPane.ERROR_MESSAGE); - this.actionPerformed(e); // show the dialog box again - } else { - // try to close Startup window if there's one - try { - StartupWindowProvider.getInstance().close(); - } catch (Exception ex) { - // no need to show the error message to the user. - logger.log(Level.WARNING, "Error closing startup window.", ex); //NON-NLS - } - try { - Case.open(path); // open the case - } catch (CaseActionException ex) { - JOptionPane.showMessageDialog(null, - NbBundle.getMessage(this.getClass(), - "CaseOpenAction.msgDlg.cantOpenCase.msg", path, - ex.getMessage()), - NbBundle.getMessage(this.getClass(), - "CaseOpenAction.msgDlg.cantOpenCase.title"), - JOptionPane.ERROR_MESSAGE); - logger.log(Level.WARNING, "Error opening case in folder " + path, ex); //NON-NLS - - StartupWindowProvider.getInstance().open(); - } + /** + * This is a bit of a hack, but close the startup window, if it was + * the source of the action invocation. + */ + try { + StartupWindowProvider.getInstance().close(); + } catch (Exception unused) { } + + /** + * Try to open the caswe associated with the case meta data file the + * user selected. + */ + final String path = fileChooser.getSelectedFile().getPath(); + String dirPath = fileChooser.getSelectedFile().getParent(); + ModuleSettings.setConfigSetting(ModuleSettings.MAIN_SETTINGS, PROP_BASECASE, dirPath.substring(0, dirPath.lastIndexOf(File.separator))); + new Thread(() -> { + try { + Case.open(path); + } catch (CaseActionException ex) { + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog(null, ex.getMessage(), NbBundle.getMessage(this.getClass(), "CaseOpenAction.msgDlg.cantOpenCase.title"), JOptionPane.ERROR_MESSAGE); + if (!Case.isCaseOpen()) { + StartupWindowProvider.getInstance().open(); + } + }); + } + }).start(); } } + } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseSchema.xsd b/Core/src/org/sleuthkit/autopsy/casemodule/CaseSchema.xsd index 30a86d859a..63c9aa49df 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseSchema.xsd +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseSchema.xsd @@ -12,6 +12,12 @@ + + + + + + @@ -92,6 +98,11 @@ + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CollaborationMonitor.java b/Core/src/org/sleuthkit/autopsy/casemodule/CollaborationMonitor.java new file mode 100644 index 0000000000..4ea3463acc --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CollaborationMonitor.java @@ -0,0 +1,601 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-2015 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; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.io.Serializable; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import org.netbeans.api.progress.ProgressHandle; +import org.netbeans.api.progress.ProgressHandleFactory; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.events.AddingDataSourceEvent; +import org.sleuthkit.autopsy.casemodule.events.AddingDataSourceFailedEvent; +import org.sleuthkit.autopsy.casemodule.events.DataSourceAddedEvent; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.NetworkUtils; +import org.sleuthkit.autopsy.events.AutopsyEvent; +import org.sleuthkit.autopsy.events.AutopsyEventException; +import org.sleuthkit.autopsy.events.AutopsyEventPublisher; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.autopsy.ingest.events.DataSourceAnalysisCompletedEvent; +import org.sleuthkit.autopsy.ingest.events.DataSourceAnalysisStartedEvent; + +/** + * A collaboration monitor listens to local events and translates them into + * collaboration tasks that are broadcast to collaborating nodes and informs the + * user of collaboration tasks on other nodes using progress bars. + */ +final class CollaborationMonitor { + + private static final String EVENT_CHANNEL_NAME = "%s-Collaboration-Monitor-Events"; + private static final String COLLABORATION_MONITOR_EVENT = "COLLABORATION_MONITOR_EVENT"; + private static final Set CASE_EVENTS_OF_INTEREST = new HashSet<>(Arrays.asList(new String[]{Case.Events.ADDING_DATA_SOURCE.toString(), Case.Events.DATA_SOURCE_ADDED.toString(), Case.Events.ADDING_DATA_SOURCE_FAILED.toString()})); + private static final int NUMBER_OF_PERIODIC_TASK_THREADS = 2; + private static final String PERIODIC_TASK_THREAD_NAME = "collab-monitor-periodic-tasks-%d"; + private static final long HEARTBEAT_INTERVAL_MINUTES = 1; + private static final long MAX_MISSED_HEARTBEATS = 5; + private static final long STALE_TASKS_DETECTION_INTERVAL_MINUTES = 2; + private static final long EXECUTOR_TERMINATION_WAIT_SECS = 30; + private static final Logger logger = Logger.getLogger(CollaborationMonitor.class.getName()); + private final String hostName; + private final LocalTasksManager localTasksManager; + private final RemoteTasksManager remoteTasksManager; + private final AutopsyEventPublisher eventPublisher; + private final ScheduledThreadPoolExecutor periodicTasksExecutor; + + /** + * Constructs a collaboration monitor that listens to local events and + * translates them into collaboration tasks that are broadcast to + * collaborating nodes, informs the user of collaboration tasks on other + * nodes using progress bars, and monitors the health of key collaboration + * services. + */ + CollaborationMonitor() throws CollaborationMonitorException { + /** + * Get the local host name so it can be used to identify the source of + * collaboration tasks broadcast by this node. + */ + hostName = NetworkUtils.getLocalHostName(); + + /** + * Create an event publisher that will be used to communicate with + * collaboration monitors on other nodes working on the case. + */ + eventPublisher = new AutopsyEventPublisher(); + try { + Case openedCase = Case.getCurrentCase(); + String channelPrefix = openedCase.getTextIndexName(); + eventPublisher.openRemoteEventChannel(String.format(EVENT_CHANNEL_NAME, channelPrefix)); + } catch (AutopsyEventException ex) { + throw new CollaborationMonitorException("Failed to initialize", ex); + } + + /** + * Create a remote tasks manager to track and display the progress of + * remote tasks. + */ + remoteTasksManager = new RemoteTasksManager(); + eventPublisher.addSubscriber(COLLABORATION_MONITOR_EVENT, remoteTasksManager); + + /** + * Create a local tasks manager to track and broadcast local tasks. + */ + localTasksManager = new LocalTasksManager(); + IngestManager.getInstance().addIngestJobEventListener(localTasksManager); + Case.addEventSubscriber(CASE_EVENTS_OF_INTEREST, localTasksManager); + + /** + * Start periodic tasks that: + * + * 1. Send heartbeats to collaboration monitors on other nodes.
+ * 2. Check for stale remote tasks.
+ */ + periodicTasksExecutor = new ScheduledThreadPoolExecutor(NUMBER_OF_PERIODIC_TASK_THREADS, new ThreadFactoryBuilder().setNameFormat(PERIODIC_TASK_THREAD_NAME).build()); + periodicTasksExecutor.scheduleAtFixedRate(new HeartbeatTask(), HEARTBEAT_INTERVAL_MINUTES, HEARTBEAT_INTERVAL_MINUTES, TimeUnit.MINUTES); + periodicTasksExecutor.scheduleAtFixedRate(new StaleTaskDetectionTask(), STALE_TASKS_DETECTION_INTERVAL_MINUTES, STALE_TASKS_DETECTION_INTERVAL_MINUTES, TimeUnit.MINUTES); + } + + /** + * Shuts down this collaboration monitor. + */ + void shutdown() { + if (null != periodicTasksExecutor) { + periodicTasksExecutor.shutdownNow(); + try { + while (!periodicTasksExecutor.awaitTermination(EXECUTOR_TERMINATION_WAIT_SECS, TimeUnit.SECONDS)) { + logger.log(Level.WARNING, "Waited at least {0} seconds for periodic tasks executor to shut down, continuing to wait", EXECUTOR_TERMINATION_WAIT_SECS); //NON-NLS + } + } catch (InterruptedException ex) { + logger.log(Level.SEVERE, "Unexpected interrupt while stopping periodic tasks executor", ex); //NON-NLS + } + } + + Case.removeEventSubscriber(CASE_EVENTS_OF_INTEREST, localTasksManager); + IngestManager.getInstance().removeIngestJobEventListener(localTasksManager); + + if (null != eventPublisher) { + eventPublisher.removeSubscriber(COLLABORATION_MONITOR_EVENT, remoteTasksManager); + eventPublisher.closeRemoteEventChannel(); + } + + remoteTasksManager.shutdown(); + } + + /** + * The local tasks manager listens to local events and translates them into + * tasks it broadcasts to collaborating nodes. Note that all access to the + * task collections is synchronized since they may be accessed by both the + * threads publishing property change events and by the heartbeat task + * thread. + */ + private final class LocalTasksManager implements PropertyChangeListener { + + private long nextTaskId; + private final Map uuidsToAddDataSourceTasks; + private final Map jobIdsTodataSourceAnalysisTasks; + + /** + * Constructs a local tasks manager that listens to local events and + * translates them into tasks that can be broadcast to collaborating + * nodes. + */ + LocalTasksManager() { + nextTaskId = 0; + uuidsToAddDataSourceTasks = new HashMap<>(); + jobIdsTodataSourceAnalysisTasks = new HashMap<>(); + } + + /** + * Translates events into updates of the collection of local tasks this + * node is broadcasting to other nodes. + * + * @param event A PropertyChangeEvent. + */ + @Override + public void propertyChange(PropertyChangeEvent event) { + if (AutopsyEvent.SourceType.LOCAL == ((AutopsyEvent) event).getSourceType()) { + String eventName = event.getPropertyName(); + if (eventName.equals(Case.Events.ADDING_DATA_SOURCE.toString())) { + addDataSourceAddTask((AddingDataSourceEvent) event); + } else if (eventName.equals(Case.Events.ADDING_DATA_SOURCE_FAILED.toString())) { + removeDataSourceAddTask(((AddingDataSourceFailedEvent) event).getDataSourceId()); + } else if (eventName.equals(Case.Events.DATA_SOURCE_ADDED.toString())) { + removeDataSourceAddTask(((DataSourceAddedEvent) event).getDataSourceId()); + } else if (eventName.equals(IngestManager.IngestJobEvent.DATA_SOURCE_ANALYSIS_STARTED.toString())) { + addDataSourceAnalysisTask((DataSourceAnalysisStartedEvent) event); + } else if (eventName.equals(IngestManager.IngestJobEvent.DATA_SOURCE_ANALYSIS_COMPLETED.toString())) { + removeDataSourceAnalysisTask((DataSourceAnalysisCompletedEvent) event); + } + } + } + + /** + * Adds an adding data source task to the collection of local tasks and + * publishes the updated collection to any collaborating nodes. + * + * @param event An adding data source event. + */ + synchronized void addDataSourceAddTask(AddingDataSourceEvent event) { + String status = NbBundle.getMessage(CollaborationMonitor.class, "CollaborationMonitor.addingDataSourceStatus.msg", hostName); + uuidsToAddDataSourceTasks.put(event.getDataSourceId().hashCode(), new Task(++nextTaskId, status)); + eventPublisher.publishRemotely(new CollaborationEvent(hostName, getCurrentTasks())); + } + + /** + * Removes an adding data source task from the collection of local tasks + * and publishes the updated collection to any collaborating nodes. + * + * @param dataSourceId A data source id to pair a data source added or + * adding data source failed event with an adding + * data source event. + */ + synchronized void removeDataSourceAddTask(UUID dataSourceId) { + uuidsToAddDataSourceTasks.remove(dataSourceId.hashCode()); + eventPublisher.publishRemotely(new CollaborationEvent(hostName, getCurrentTasks())); + } + + /** + * Adds a data source analysis task to the collection of local tasks and + * publishes the updated collection to any collaborating nodes. + * + * @param event A data source analysis started event. + */ + synchronized void addDataSourceAnalysisTask(DataSourceAnalysisStartedEvent event) { + String status = NbBundle.getMessage(CollaborationMonitor.class, "CollaborationMonitor.analyzingDataSourceStatus.msg", hostName, event.getDataSource().getName()); + jobIdsTodataSourceAnalysisTasks.put(event.getDataSourceIngestJobId(), new Task(++nextTaskId, status)); + eventPublisher.publishRemotely(new CollaborationEvent(hostName, getCurrentTasks())); + } + + /** + * Removes a data source analysis task from the collection of local + * tasks and publishes the updated collection to any collaborating + * nodes. + * + * @param event A data source analysis completed event. + */ + synchronized void removeDataSourceAnalysisTask(DataSourceAnalysisCompletedEvent event) { + jobIdsTodataSourceAnalysisTasks.remove(event.getDataSourceIngestJobId()); + eventPublisher.publishRemotely(new CollaborationEvent(hostName, getCurrentTasks())); + } + + /** + * Gets the current local tasks. + * + * @return A mapping of task IDs to tasks, may be empty. + */ + synchronized Map getCurrentTasks() { + Map currentTasks = new HashMap<>(); + uuidsToAddDataSourceTasks.values().stream().forEach((task) -> { + currentTasks.put(task.getId(), task); + }); + jobIdsTodataSourceAnalysisTasks.values().stream().forEach((task) -> { + currentTasks.put(task.getId(), task); + }); + return currentTasks; + } + } + + /** + * Listens for collaboration event messages broadcast by collaboration + * monitors on other nodes and translates them into remote tasks represented + * locally using progress bars. Note that all access to the remote tasks is + * synchronized since it may be accessed by both the threads publishing + * property change events and by the thread running periodic checks for + * "stale" tasks. + */ + private final class RemoteTasksManager implements PropertyChangeListener { + + private final Map hostsToTasks; + + /** + * Constructs an object that listens for collaboration event messages + * broadcast by collaboration monitors on other nodes and translates + * them into remote tasks represented locally using progress bars. + */ + RemoteTasksManager() { + hostsToTasks = new HashMap<>(); + } + + /** + * Updates the remote tasks in response to a collaboration event + * received from another node. + * + * @param event The collaboration event. + */ + @Override + public void propertyChange(PropertyChangeEvent event) { + if (event.getPropertyName().equals(COLLABORATION_MONITOR_EVENT)) { + updateTasks((CollaborationEvent) event); + } + } + + /** + * Finishes the progress bars for all remote tasks. + */ + synchronized void shutdown() { + finishAllTasks(); + } + + /** + * Updates the remote tasks to reflect a collaboration event received + * from another node. + * + * @param event The collaboration event. + */ + synchronized void updateTasks(CollaborationEvent event) { + RemoteTasks tasksForHost = hostsToTasks.get(event.getHostName()); + if (null != tasksForHost) { + tasksForHost.update(event); + } else { + hostsToTasks.put(event.getHostName(), new RemoteTasks(event)); + } + } + + /** + * Finishes the progress bars any remote tasks that have gone stale, + * i.e., tasks for which updates have ceased, presumably because the + * collaborating node has gone down or there is a network issue. + */ + synchronized void finishStaleTasks() { + for (Iterator> it = hostsToTasks.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = it.next(); + RemoteTasks tasksForHost = entry.getValue(); + if (tasksForHost.isStale()) { + tasksForHost.finishAllTasks(); + it.remove(); + } + } + } + + /** + * Finishes the progress bars for all remote tasks. + */ + synchronized void finishAllTasks() { + for (Iterator> it = hostsToTasks.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = it.next(); + RemoteTasks tasksForHost = entry.getValue(); + tasksForHost.finishAllTasks(); + it.remove(); + } + } + + /** + * A collection of progress bars for tasks on a collaborating node. + */ + private final class RemoteTasks { + + private final long MAX_MINUTES_WITHOUT_UPDATE = HEARTBEAT_INTERVAL_MINUTES * MAX_MISSED_HEARTBEATS; + private Instant lastUpdateTime; + private Map taskIdsToProgressBars; + + /** + * Construct a set of progress bars to represent remote tasks for a + * particular host. + * + * @param event A collaboration event. + */ + RemoteTasks(CollaborationEvent event) { + /** + * Set the initial value of the last update time stamp. + */ + lastUpdateTime = Instant.now(); + + taskIdsToProgressBars = new HashMap<>(); + event.getCurrentTasks().values().stream().forEach((task) -> { + ProgressHandle progress = ProgressHandleFactory.createHandle(event.getHostName()); + progress.start(); + progress.progress(task.getStatus()); + taskIdsToProgressBars.put(task.getId(), progress); + }); + } + + /** + * Updates this remote tasks collection. + * + * @param event A collaboration event from the collaborating node + * associated with these tasks. + */ + void update(CollaborationEvent event) { + /** + * Update the last update timestamp. + */ + lastUpdateTime = Instant.now(); + + /** + * Create or update the progress bars for the current tasks of + * the node that published the event. + */ + Map remoteTasks = event.getCurrentTasks(); + remoteTasks.values().stream().forEach((task) -> { + ProgressHandle progress = taskIdsToProgressBars.get(task.getId()); + if (null != progress) { + /** + * Update the existing progress bar. + */ + progress.progress(task.getStatus()); + } else { + /** + * A new task, create a progress bar. + */ + progress = ProgressHandleFactory.createHandle(event.getHostName()); + progress.start(); + progress.progress(task.getStatus()); + taskIdsToProgressBars.put(task.getId(), progress); + } + }); + + /** + * If a task is no longer in the task list from the remote node, + * it is finished. Remove the progress bars for finished tasks. + */ + for (Iterator> iterator = taskIdsToProgressBars.entrySet().iterator(); iterator.hasNext();) { + Map.Entry entry = iterator.next(); + if (!remoteTasks.containsKey(entry.getKey())) { + ProgressHandle progress = entry.getValue(); + progress.finish(); + iterator.remove(); + } + } + } + + /** + * Unconditionally finishes the entire set or remote tasks. To be + * used when a host drops off unexpectedly. + */ + void finishAllTasks() { + taskIdsToProgressBars.values().stream().forEach((progress) -> { + progress.finish(); + }); + taskIdsToProgressBars.clear(); + } + + /** + * Determines whether or not the time since the last update of this + * remote tasks collection is greater than the maximum acceptable + * interval between updates. + * + * @return True or false. + */ + boolean isStale() { + return Duration.between(lastUpdateTime, Instant.now()).toMinutes() >= MAX_MINUTES_WITHOUT_UPDATE; + } + } + + } + + /** + * A Runnable task that periodically publishes the local tasks in progress + * on this node, providing a heartbeat message for collaboration monitors on + * other nodes. The current local tasks are included in the heartbeat + * message so that nodes that have just joined the event channel know what + * this node is doing, even if they join after the current tasks are begun. + */ + private final class HeartbeatTask implements Runnable { + + /** + * Publish a heartbeat message. + */ + @Override + public void run() { + eventPublisher.publishRemotely(new CollaborationEvent(hostName, localTasksManager.getCurrentTasks())); + } + } + + /** + * A Runnable task that periodically deals with any remote tasks that have + * gone stale, i.e., tasks for which updates have ceased, presumably because + * the collaborating node has gone down or there is a network issue. + */ + private final class StaleTaskDetectionTask implements Runnable { + + /** + * Check for stale remote tasks and clean them up, if found. + */ + @Override + public void run() { + remoteTasksManager.finishStaleTasks(); + } + } + + /** + * An Autopsy event to be sent in event messages to the collaboration + * monitors on other Autopsy nodes. + */ + private final static class CollaborationEvent extends AutopsyEvent implements Serializable { + + private static final long serialVersionUID = 1L; + private final String hostName; + private final Map currentTasks; + + /** + * Constructs an Autopsy event to be sent in event messages to the + * collaboration monitors on other Autopsy nodes. + * + * @param hostName The name of the host sending the event. + * @param currentTasks The tasks in progress for this Autopsy node. + */ + CollaborationEvent(String hostName, Map currentTasks) { + super(COLLABORATION_MONITOR_EVENT, null, null); + this.hostName = hostName; + this.currentTasks = currentTasks; + } + + /** + * Gets the host name of the Autopsy node that published this event. + * + * @return The host name. + */ + String getHostName() { + return hostName; + } + + /** + * Gets the current tasks for the Autopsy node that published this + * event. + * + * @return A mapping of task IDs to current tasks + */ + Map getCurrentTasks() { + return currentTasks; + } + + } + + /** + * A representation of a task in progress on this Autopsy node. + */ + private final static class Task implements Serializable { + + private static final long serialVersionUID = 1L; + private final long id; + private final String status; + + /** + * Constructs a representation of a task in progress on this Autopsy + * node. + * + * @param id + * @param status + */ + Task(long id, String status) { + this.id = id; + this.status = status; + } + + /** + * Gets ID of this task. + * + * @return A task id, unique to this task for this case and this Autopsy + * node. + */ + long getId() { + return id; + } + + /** + * Gets the status of the task at the time this object was constructed. + * + * @return A task status string. + */ + String getStatus() { + return status; + } + } + + /** + * Custom exception class for the collaboration monitor. + */ + final static class CollaborationMonitorException extends Exception { + + /** + * Constructs and instance of the custom exception class for the + * collaboration monitor. + * + * @param message Exception message. + */ + CollaborationMonitorException(String message) { + super(message); + } + + /** + * Constructs and instance of the custom exception class for the + * collaboration monitor. + * + * @param message Exception message. + * @param throwable Exception cause. + */ + CollaborationMonitorException(String message, Throwable throwable) { + super(message, throwable); + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CueBannerPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/CueBannerPanel.java index 793629da66..440d0b4d20 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CueBannerPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CueBannerPanel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011 Basis Technology Corp. + * Copyright 2011-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,16 +18,18 @@ */ package org.sleuthkit.autopsy.casemodule; -import java.awt.*; import java.awt.Dialog; import java.awt.Dimension; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; import javax.swing.ImageIcon; +import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JPanel; +import javax.swing.KeyStroke; import org.openide.util.Lookup; import org.openide.util.NbBundle; @@ -42,7 +44,6 @@ public class CueBannerPanel extends javax.swing.JPanel { // for error handling private static JPanel caller = new JPanel(); - private String className = this.getClass().toString(); public CueBannerPanel() { initComponents(); @@ -209,6 +210,10 @@ public class CueBannerPanel extends javax.swing.JPanel { // set the location of the popUp Window on the center of the screen recentCasesWindow.setLocation((screenDimension.width - w) / 2, (screenDimension.height - h) / 2); + recentCasesWindow.setLocationRelativeTo(this); + recentCasesWindow.getRootPane().registerKeyboardAction(e -> { + recentCasesWindow.dispose(); + }, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); OpenRecentCasePanel welcomeWindow = OpenRecentCasePanel.getInstance(); diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.form b/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.form index b4dbaafd63..bfccc9bdd9 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.form +++ b/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.form @@ -43,6 +43,7 @@ + @@ -57,7 +58,9 @@ - + + + @@ -66,7 +69,7 @@ - + @@ -131,5 +134,15 @@
+ + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.java index a1a285ede9..4d59cd4d91 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.java @@ -37,6 +37,7 @@ import org.sleuthkit.autopsy.coreutils.ModuleSettings; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import java.util.logging.Level; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.PathValidator; /** * ImageTypePanel for adding an image file such as .img, .E0x, .00x, etc. @@ -65,6 +66,8 @@ public class ImageFilePanel extends JPanel implements DocumentListener { fc.setFileSelectionMode(JFileChooser.FILES_ONLY); fc.setMultiSelectionEnabled(false); + errorLabel.setVisible(false); + boolean firstFilter = true; for (FileFilter filter : fileChooserFilters) { if (firstFilter) { // set the first on the list as the default selection @@ -115,6 +118,7 @@ public class ImageFilePanel extends JPanel implements DocumentListener { timeZoneComboBox = new javax.swing.JComboBox(); noFatOrphansCheckbox = new javax.swing.JCheckBox(); descLabel = new javax.swing.JLabel(); + errorLabel = new javax.swing.JLabel(); setMinimumSize(new java.awt.Dimension(0, 65)); setPreferredSize(new java.awt.Dimension(403, 65)); @@ -139,6 +143,9 @@ public class ImageFilePanel extends JPanel implements DocumentListener { org.openide.awt.Mnemonics.setLocalizedText(descLabel, org.openide.util.NbBundle.getMessage(ImageFilePanel.class, "ImageFilePanel.descLabel.text")); // NOI18N + errorLabel.setForeground(new java.awt.Color(255, 0, 0)); + org.openide.awt.Mnemonics.setLocalizedText(errorLabel, org.openide.util.NbBundle.getMessage(ImageFilePanel.class, "ImageFilePanel.errorLabel.text")); // NOI18N + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( @@ -158,7 +165,8 @@ public class ImageFilePanel extends JPanel implements DocumentListener { .addComponent(noFatOrphansCheckbox) .addGroup(layout.createSequentialGroup() .addGap(21, 21, 21) - .addComponent(descLabel))) + .addComponent(descLabel)) + .addComponent(errorLabel)) .addGap(0, 20, Short.MAX_VALUE)) ); layout.setVerticalGroup( @@ -169,7 +177,9 @@ public class ImageFilePanel extends JPanel implements DocumentListener { .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(browseButton) .addComponent(pathTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addGap(18, 18, 18) + .addGap(3, 3, 3) + .addComponent(errorLabel) + .addGap(1, 1, 1) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(timeZoneLabel) .addComponent(timeZoneComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) @@ -177,7 +187,7 @@ public class ImageFilePanel extends JPanel implements DocumentListener { .addComponent(noFatOrphansCheckbox) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(descLabel) - .addContainerGap(13, Short.MAX_VALUE)) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); }// //GEN-END:initComponents @@ -208,6 +218,7 @@ public class ImageFilePanel extends JPanel implements DocumentListener { // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JButton browseButton; private javax.swing.JLabel descLabel; + private javax.swing.JLabel errorLabel; private javax.swing.JCheckBox noFatOrphansCheckbox; private javax.swing.JLabel pathLabel; private javax.swing.JTextField pathTextField; @@ -252,11 +263,15 @@ public class ImageFilePanel extends JPanel implements DocumentListener { * @return true if a proper image has been selected, false otherwise */ public boolean validatePanel() { + errorLabel.setVisible(false); String path = getContentPaths(); if (path == null || path.isEmpty()) { return false; } + // display warning if there is one (but don't disable "next" button) + warnIfPathIsInvalid(path); + boolean isExist = Case.pathExists(path); boolean isPhysicalDrive = Case.isPhysicalDrive(path); boolean isPartition = Case.isPartition(path); @@ -264,6 +279,19 @@ public class ImageFilePanel extends JPanel implements DocumentListener { return (isExist || isPhysicalDrive || isPartition); } + /** + * Validates path to selected data source and displays warning if it is + * invalid. + * + * @param path Absolute path to the selected data source + */ + private void warnIfPathIsInvalid(String path) { + if (!PathValidator.isValid(path, Case.getCurrentCase().getCaseType())) { + errorLabel.setVisible(true); + errorLabel.setText(NbBundle.getMessage(this.getClass(), "DataSourceOnCDriveError.text")); + } + } + public void storeSettings() { String imagePathName = getContentPaths(); if (null != imagePathName) { diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/ImportDoneCallback.java b/Core/src/org/sleuthkit/autopsy/casemodule/ImportDoneCallback.java new file mode 100644 index 0000000000..bb83712cc3 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/ImportDoneCallback.java @@ -0,0 +1,6 @@ +package org.sleuthkit.autopsy.casemodule; + +public interface ImportDoneCallback { + + void importDoneCallback(boolean result, String resultString); +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/IntervalErrorReportData.java b/Core/src/org/sleuthkit/autopsy/casemodule/IntervalErrorReportData.java new file mode 100644 index 0000000000..52ea056333 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/IntervalErrorReportData.java @@ -0,0 +1,92 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-2015 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; + +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; + +/** + * This class enables capturing errors and batching them for reporting on a + * no-more-than-x number of seconds basis. When created, you specify the minimum + * time between user notifications. When the time between notifications has + * expired, the next error encountered will cause a report to be shown to the + * user. + */ +class IntervalErrorReportData { + + private final Case currentCase; + private long newProblems; + private long totalProblems; + private long lastReportedDate; + private final int milliSecondsBetweenReports; + private final String message; + + /** + * Create a new IntervalErrorReprotData instance and subscribe for TSK error + * notifications for the current case. + * + * @param currentCase Case for which TSK errors should be tracked + * and displayed. + * @param secondsBetweenReports Minimum number of seconds between reports. + * It will not warn more frequently than this. + * @param message The message that will be shown when warning + * the user + */ + IntervalErrorReportData(Case currentCase, int secondsBetweenReports, String message) { + this.newProblems = 0; + this.totalProblems = 0; + this.lastReportedDate = 0; // arm the first warning by choosing zero + this.milliSecondsBetweenReports = secondsBetweenReports * 1000; // convert to milliseconds + this.message = message; + this.currentCase = currentCase; + this.currentCase.getSleuthkitCase().addErrorObserver(this.currentCase); + } + + /** + * Un-subscribe from TSK error notifications for current case. + */ + void shutdown() { + this.currentCase.getSleuthkitCase().removeErrorObserver(this.currentCase); + } + + /** + * Call this to add problems to the class. When the time threshold is met + * (or if this is the first problem encountered), a warning will be shown to + * the user. + * + * @param context The context in which the error occurred. + * @param errorMessage A description of the error that occurred. + */ + void addProblems(String context, String errorMessage) { + this.newProblems += 1; + this.totalProblems += newProblems; + + long currentTimeStamp = System.currentTimeMillis(); + if ((currentTimeStamp - lastReportedDate) > milliSecondsBetweenReports) { + this.lastReportedDate = currentTimeStamp; + MessageNotifyUtil.Notify.error(message, context + ", " + errorMessage + " " + + this.newProblems + " " + + NbBundle.getMessage(IntervalErrorReportData.class, "IntervalErrorReport.NewIssues") + + " " + this.totalProblems + " " + + NbBundle.getMessage(IntervalErrorReportData.class, "IntervalErrorReport.TotalIssues") + + "."); + this.newProblems = 0; + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.form b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.form index d71086457c..55707d9f0c 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.form +++ b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.form @@ -61,7 +61,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.java index 4502a8dde5..669ffad552 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.java @@ -93,9 +93,9 @@ final class LocalDiskPanel extends JPanel { diskComboBox.setModel(model); diskComboBox.setRenderer(model); + errorLabel.setVisible(false); errorLabel.setText(""); diskComboBox.setEnabled(false); - } /** @@ -167,7 +167,7 @@ final class LocalDiskPanel extends JPanel { .addComponent(noFatOrphansCheckbox) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(descLabel) - .addContainerGap(21, Short.MAX_VALUE)) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); }// //GEN-END:initComponents // Variables declaration - do not modify//GEN-BEGIN:variables diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesPanel.form b/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesPanel.form index cf08c9c38f..eb9d6182bb 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesPanel.form +++ b/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesPanel.form @@ -60,6 +60,10 @@ + + + + @@ -67,15 +71,16 @@ - + + - + - - + + @@ -136,5 +141,15 @@ + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesPanel.java index 15b042bbc1..eae470e0b4 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesPanel.java @@ -21,6 +21,8 @@ package org.sleuthkit.autopsy.casemodule; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.File; +import java.util.Arrays; +import java.util.List; import java.util.Set; import java.util.TreeSet; import javax.swing.JFileChooser; @@ -30,7 +32,9 @@ import org.openide.util.NbBundle; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import java.util.logging.Level; +import org.sleuthkit.autopsy.casemodule.Case.CaseType; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.PathValidator; /** * Add input wizard subpanel for adding local files / dirs to the case @@ -61,8 +65,8 @@ class LocalFilesPanel extends JPanel { private void customInit() { localFileChooser.setMultiSelectionEnabled(true); + errorLabel.setVisible(false); selectedPaths.setText(""); - } //@Override @@ -93,9 +97,35 @@ class LocalFilesPanel extends JPanel { //@Override public boolean validatePanel() { + + // display warning if there is one (but don't disable "next" button) + warnIfPathIsInvalid(getContentPaths()); + return enableNext; } + /** + * Validates path to selected data source and displays warning if it is + * invalid. + * + * @param path Absolute path to the selected data source + */ + private void warnIfPathIsInvalid(String path) { + errorLabel.setVisible(false); + + // Path variable for "Local files" module is a coma separated string containg multiple paths + List pathsList = Arrays.asList(path.split(",")); + CaseType currentCaseType = Case.getCurrentCase().getCaseType(); + + for (String currentPath : pathsList) { + if (!PathValidator.isValid(currentPath, currentCaseType)) { + errorLabel.setVisible(true); + errorLabel.setText(NbBundle.getMessage(this.getClass(), "DataSourceOnCDriveError.text")); + return; + } + } + } + //@Override public void select() { reset(); @@ -106,8 +136,7 @@ class LocalFilesPanel extends JPanel { currentFiles.clear(); selectedPaths.setText(""); enableNext = false; - - //pcs.firePropertyChange(AddImageWizardChooseDataSourceVisual.EVENT.UPDATE_UI.toString(), false, true); + errorLabel.setVisible(false); } @Override @@ -150,6 +179,7 @@ class LocalFilesPanel extends JPanel { clearButton = new javax.swing.JButton(); jScrollPane2 = new javax.swing.JScrollPane(); selectedPaths = new javax.swing.JTextArea(); + errorLabel = new javax.swing.JLabel(); localFileChooser.setApproveButtonText(org.openide.util.NbBundle.getMessage(LocalFilesPanel.class, "LocalFilesPanel.localFileChooser.approveButtonText")); // NOI18N localFileChooser.setApproveButtonToolTipText(org.openide.util.NbBundle.getMessage(LocalFilesPanel.class, "LocalFilesPanel.localFileChooser.approveButtonToolTipText")); // NOI18N @@ -185,6 +215,9 @@ class LocalFilesPanel extends JPanel { selectedPaths.setToolTipText(org.openide.util.NbBundle.getMessage(LocalFilesPanel.class, "LocalFilesPanel.selectedPaths.toolTipText")); // NOI18N jScrollPane2.setViewportView(selectedPaths); + errorLabel.setForeground(new java.awt.Color(255, 0, 0)); + org.openide.awt.Mnemonics.setLocalizedText(errorLabel, org.openide.util.NbBundle.getMessage(LocalFilesPanel.class, "LocalFilesPanel.errorLabel.text")); // NOI18N + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( @@ -199,19 +232,23 @@ class LocalFilesPanel extends JPanel { .addComponent(selectButton, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(clearButton, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) .addGap(2, 2, 2)) + .addGroup(layout.createSequentialGroup() + .addComponent(errorLabel) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addComponent(infoLabel) .addGap(5, 5, 5) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(jScrollPane2, javax.swing.GroupLayout.PREFERRED_SIZE, 82, javax.swing.GroupLayout.PREFERRED_SIZE) .addGroup(layout.createSequentialGroup() .addComponent(selectButton) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 17, Short.MAX_VALUE) - .addComponent(clearButton)) - .addComponent(jScrollPane2, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE)) - .addGap(0, 0, 0)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(clearButton))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(errorLabel)) ); }// //GEN-END:initComponents @@ -256,6 +293,7 @@ class LocalFilesPanel extends JPanel { // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JButton clearButton; + private javax.swing.JLabel errorLabel; private javax.swing.JLabel infoLabel; private javax.swing.JScrollPane jScrollPane1; private javax.swing.JScrollPane jScrollPane2; diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/MissingImageDialog.form b/Core/src/org/sleuthkit/autopsy/casemodule/MissingImageDialog.form index d730da08dc..8225797395 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/MissingImageDialog.form +++ b/Core/src/org/sleuthkit/autopsy/casemodule/MissingImageDialog.form @@ -112,10 +112,15 @@ - - - - + + + + + + + + + @@ -127,7 +132,9 @@ - + + + @@ -153,6 +160,22 @@ + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/MissingImageDialog.java b/Core/src/org/sleuthkit/autopsy/casemodule/MissingImageDialog.java index 18eda3fffc..d5e5e647e5 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/MissingImageDialog.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/MissingImageDialog.java @@ -137,6 +137,7 @@ class MissingImageDialog extends javax.swing.JDialog { containerPanel = new javax.swing.JPanel(); pathNameTextField = new javax.swing.JTextField(); browseButton = new javax.swing.JButton(); + lbWarning = new javax.swing.JLabel(); titleLabel = new javax.swing.JLabel(); titleSeparator = new javax.swing.JSeparator(); @@ -191,16 +192,24 @@ class MissingImageDialog extends javax.swing.JDialog { } }); + lbWarning.setFont(new java.awt.Font("Tahoma", 1, 12)); // NOI18N + lbWarning.setForeground(new java.awt.Color(244, 0, 0)); + org.openide.awt.Mnemonics.setLocalizedText(lbWarning, org.openide.util.NbBundle.getMessage(MissingImageDialog.class, "MissingImageDialog.lbWarning.text")); // NOI18N + lbWarning.setToolTipText(org.openide.util.NbBundle.getMessage(MissingImageDialog.class, "MissingImageDialog.lbWarning.toolTipText")); // NOI18N + javax.swing.GroupLayout containerPanelLayout = new javax.swing.GroupLayout(containerPanel); containerPanel.setLayout(containerPanelLayout); containerPanelLayout.setHorizontalGroup( containerPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(containerPanelLayout.createSequentialGroup() .addContainerGap() - .addComponent(pathNameTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 285, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(browseButton) - .addContainerGap(83, Short.MAX_VALUE)) + .addGroup(containerPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(lbWarning, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(containerPanelLayout.createSequentialGroup() + .addComponent(pathNameTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 285, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(browseButton) + .addContainerGap(83, Short.MAX_VALUE)))) ); containerPanelLayout.setVerticalGroup( containerPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) @@ -209,12 +218,13 @@ class MissingImageDialog extends javax.swing.JDialog { .addGroup(containerPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(pathNameTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(browseButton)) - .addContainerGap(62, Short.MAX_VALUE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(lbWarning, javax.swing.GroupLayout.DEFAULT_SIZE, 19, Short.MAX_VALUE) + .addGap(18, 18, 18)) ); - titleLabel.setFont(titleLabel.getFont().deriveFont(Font.BOLD, 12)); - org.openide.awt.Mnemonics.setLocalizedText(titleLabel, org.openide.util.NbBundle - .getMessage(MissingImageDialog.class, "MissingImageDialog.titleLabel.text")); // NOI18N + titleLabel.setFont(new java.awt.Font("Tahoma", 1, 12)); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(titleLabel, org.openide.util.NbBundle.getMessage(MissingImageDialog.class, "MissingImageDialog.titleLabel.text")); // NOI18N titleSeparator.setForeground(new java.awt.Color(102, 102, 102)); @@ -254,10 +264,11 @@ class MissingImageDialog extends javax.swing.JDialog { String newPath = pathNameTextField.getText(); //TODO handle local files db.setImagePaths(obj_id, Arrays.asList(new String[]{newPath})); + this.dispose(); } catch (TskCoreException ex) { + lbWarning.setText(NbBundle.getMessage(this.getClass(), "MissingImageDialog.ErrorSettingImage")); logger.log(Level.WARNING, "Error setting image paths", ex); //NON-NLS } - this.dispose(); }//GEN-LAST:event_selectButtonActionPerformed private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cancelButtonActionPerformed @@ -273,7 +284,7 @@ class MissingImageDialog extends javax.swing.JDialog { private void browseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_browseButtonActionPerformed String oldText = pathNameTextField.getText(); - + lbWarning.setText(""); // set the current directory of the FileChooser if the ImagePath Field is valid File currentDir = new File(oldText); if (currentDir.exists()) { @@ -294,6 +305,7 @@ class MissingImageDialog extends javax.swing.JDialog { private javax.swing.JPanel buttonPanel; private javax.swing.JButton cancelButton; private javax.swing.JPanel containerPanel; + private javax.swing.JLabel lbWarning; private javax.swing.JTextField pathNameTextField; private javax.swing.JButton selectButton; private javax.swing.JLabel titleLabel; diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseVisualPanel1.form b/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseVisualPanel1.form index 2a2c068ccf..c30aa65b0b 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseVisualPanel1.form +++ b/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseVisualPanel1.form @@ -1,6 +1,10 @@
+ + + + @@ -16,30 +20,49 @@ - + - - - - + + + - + + + + + + + + + + + + + + + + + + - + - - - - - - - - + + + + + + + + + + + + - @@ -63,7 +86,16 @@ - + + + + + + + + + + @@ -132,5 +164,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseVisualPanel1.java b/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseVisualPanel1.java index 1c758179f1..6aebc0fe99 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseVisualPanel1.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseVisualPanel1.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011 Basis Technology Corp. + * Copyright 2011-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,25 +24,51 @@ import java.awt.*; import java.io.File; import javax.swing.JFileChooser; import javax.swing.JPanel; -import javax.swing.JTextField; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; +import org.sleuthkit.autopsy.casemodule.Case.CaseType; +import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.autopsy.coreutils.PathValidator; /** - * The wizard panel for the new case creation. - * - * @author jantonius + * The JPanel for the first page of the new case wizard. */ final class NewCaseVisualPanel1 extends JPanel implements DocumentListener { - private JFileChooser fc = new JFileChooser(); - private NewCaseWizardPanel1 wizPanel; + private final JFileChooser fileChooser = new JFileChooser(); + private final NewCaseWizardPanel1 wizPanel; + /** + * Constructs the JPanel for the first page of the new case wizard. + * + * @param wizPanel The wizard panmel that owns this panel. + */ NewCaseVisualPanel1(NewCaseWizardPanel1 wizPanel) { - initComponents(); this.wizPanel = wizPanel; - caseNameTextField.getDocument().addDocumentListener(this); - caseParentDirTextField.getDocument().addDocumentListener(this); + initComponents(); + TextFieldListener listener = new TextFieldListener(); + caseNameTextField.getDocument().addDocumentListener(listener); + caseParentDirTextField.getDocument().addDocumentListener(listener); + caseParentDirWarningLabel.setVisible(false); + } + + /** + * Should be called by the readSettings() of the wizard panel that owns this + * UI panel so that this panel can read settings for each invocation of the + * wizard as well. + */ + void readSettings() { + caseNameTextField.setText(""); + if (UserPreferences.getIsMultiUserModeEnabled()) { + multiUserCaseRadioButton.setEnabled(true); + multiUserCaseRadioButton.setSelected(true); + multiUserSettingsWarningLabel.setVisible(false); + } else { + multiUserCaseRadioButton.setEnabled(false); + singleUserCaseRadioButton.setSelected(true); + multiUserSettingsWarningLabel.setText(NbBundle.getMessage(this.getClass(), "NewCaseVisualPanel1.MultiUserDisabled.text")); + } + validateSettings(); } /** @@ -61,27 +87,112 @@ final class NewCaseVisualPanel1 extends JPanel implements DocumentListener { * * @return caseName the case name from the case name text field */ - public String getCaseName() { + String getCaseName() { return this.caseNameTextField.getText(); } + /** + * Allows the the wizard panel that owns this UI panel to set the base case + * directory to a persisted vlaue. + * + * @param caseParentDir The persisted path to the base case directory. + */ + void setCaseParentDir(String caseParentDir) { + caseParentDirTextField.setText(caseParentDir); + validateSettings(); + } + /** * Gets the base directory that the user typed on the base directory text * field. Will add file separator if it was not added. * * @return baseDirectory the base directory from the case dir text field */ - public String getCaseParentDir() { + String getCaseParentDir() { String parentDir = this.caseParentDirTextField.getText(); - if (parentDir.endsWith(File.separator) == false) { parentDir = parentDir + File.separator; } return parentDir; } - public JTextField getCaseParentDirTextField() { - return this.caseParentDirTextField; + /** + * Gets the case type. + * + * @return CaseType as set via radio buttons + */ + CaseType getCaseType() { + CaseType value = CaseType.SINGLE_USER_CASE; + if (singleUserCaseRadioButton.isSelected()) { + value = CaseType.SINGLE_USER_CASE; + } else if (multiUserCaseRadioButton.isSelected()) { + value = CaseType.MULTI_USER_CASE; + } + return value; + } + + /** + * Called when the user interacts with a child UI component of this panel, + * this method notifies the wizard panel that owns this panel and then + * validates the user's settings. + */ + private void handleUpdate() { + wizPanel.fireChangeEvent(); + validateSettings(); + } + + /** + * Does validation of the current settings and enables or disables the + * "Next" button of the wizard panel that owns this panel. + */ + private void validateSettings() { + /** + * Check the base case directory for the selected case type and show a + * warning if it is a dubious choice. + */ + caseParentDirWarningLabel.setVisible(false); + String parentDir = getCaseParentDir(); + if (!PathValidator.isValid(parentDir, getCaseType())) { + caseParentDirWarningLabel.setVisible(true); + caseParentDirWarningLabel.setText(NbBundle.getMessage(this.getClass(), "NewCaseVisualPanel1.CaseFolderOnCDriveError.text")); + } + + /** + * Enable the "Next" button for the wizard if there is text entered for + * the case name and base case directory. Also make sure that multi-user + * cases are enabled if the multi-user case radio button is selected. + */ + String caseName = getCaseName(); + if (!caseName.equals("") && !parentDir.equals("")) { + caseDirTextField.setText(parentDir + caseName); + wizPanel.setIsFinish(true); + } else { + caseDirTextField.setText(""); + wizPanel.setIsFinish(false); + } + } + + /** + * Handles validation when the user provides input to text field components + * of this panel. + */ + private class TextFieldListener implements DocumentListener { + + @Override + public void insertUpdate(DocumentEvent e) { + handleUpdate(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + handleUpdate(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + handleUpdate(); + } + } /** @@ -92,6 +203,7 @@ final class NewCaseVisualPanel1 extends JPanel implements DocumentListener { // //GEN-BEGIN:initComponents private void initComponents() { + caseTypeButtonGroup = new javax.swing.ButtonGroup(); jLabel1 = new javax.swing.JLabel(); caseNameLabel = new javax.swing.JLabel(); caseDirLabel = new javax.swing.JLabel(); @@ -100,10 +212,13 @@ final class NewCaseVisualPanel1 extends JPanel implements DocumentListener { caseDirBrowseButton = new javax.swing.JButton(); jLabel2 = new javax.swing.JLabel(); caseDirTextField = new javax.swing.JTextField(); + singleUserCaseRadioButton = new javax.swing.JRadioButton(); + multiUserCaseRadioButton = new javax.swing.JRadioButton(); + multiUserSettingsWarningLabel = new javax.swing.JLabel(); + caseParentDirWarningLabel = new javax.swing.JLabel(); - jLabel1.setFont(jLabel1.getFont().deriveFont(Font.BOLD, 14)); - org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle - .getMessage(NewCaseVisualPanel1.class, "NewCaseVisualPanel1.jLabel1.text_1")); // NOI18N + jLabel1.setFont(new java.awt.Font("Tahoma", 1, 14)); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle.getMessage(NewCaseVisualPanel1.class, "NewCaseVisualPanel1.jLabel1.text_1")); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(caseNameLabel, org.openide.util.NbBundle.getMessage(NewCaseVisualPanel1.class, "NewCaseVisualPanel1.caseNameLabel.text_1")); // NOI18N @@ -125,6 +240,28 @@ final class NewCaseVisualPanel1 extends JPanel implements DocumentListener { caseDirTextField.setEditable(false); caseDirTextField.setText(org.openide.util.NbBundle.getMessage(NewCaseVisualPanel1.class, "NewCaseVisualPanel1.caseDirTextField.text_1")); // NOI18N + caseTypeButtonGroup.add(singleUserCaseRadioButton); + org.openide.awt.Mnemonics.setLocalizedText(singleUserCaseRadioButton, org.openide.util.NbBundle.getMessage(NewCaseVisualPanel1.class, "NewCaseVisualPanel1.singleUserCaseRadioButton.text")); // NOI18N + singleUserCaseRadioButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + singleUserCaseRadioButtonActionPerformed(evt); + } + }); + + caseTypeButtonGroup.add(multiUserCaseRadioButton); + org.openide.awt.Mnemonics.setLocalizedText(multiUserCaseRadioButton, org.openide.util.NbBundle.getMessage(NewCaseVisualPanel1.class, "NewCaseVisualPanel1.multiUserCaseRadioButton.text")); // NOI18N + multiUserCaseRadioButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + multiUserCaseRadioButtonActionPerformed(evt); + } + }); + + multiUserSettingsWarningLabel.setForeground(new java.awt.Color(255, 0, 0)); + org.openide.awt.Mnemonics.setLocalizedText(multiUserSettingsWarningLabel, org.openide.util.NbBundle.getMessage(NewCaseVisualPanel1.class, "NewCaseVisualPanel1.multiUserSettingsWarningLabel.text")); // NOI18N + + caseParentDirWarningLabel.setForeground(new java.awt.Color(255, 0, 0)); + org.openide.awt.Mnemonics.setLocalizedText(caseParentDirWarningLabel, org.openide.util.NbBundle.getMessage(NewCaseVisualPanel1.class, "NewCaseVisualPanel1.caseParentDirWarningLabel.text")); // NOI18N + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( @@ -132,22 +269,35 @@ final class NewCaseVisualPanel1 extends JPanel implements DocumentListener { .addGroup(layout.createSequentialGroup() .addContainerGap() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(jLabel2) .addGroup(layout.createSequentialGroup() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false) - .addComponent(jLabel1, javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup() - .addComponent(caseDirLabel) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jLabel2) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(caseDirTextField, javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup() + .addComponent(jLabel1) + .addGap(0, 227, Short.MAX_VALUE)) + .addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup() + .addComponent(caseDirLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(caseParentDirTextField)) + .addGroup(layout.createSequentialGroup() + .addComponent(caseNameLabel) + .addGap(26, 26, 26) + .addComponent(caseNameTextField)) + .addComponent(multiUserSettingsWarningLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(caseParentDirTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 296, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup() - .addComponent(caseNameLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(caseNameTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 296, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addComponent(caseDirTextField, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.PREFERRED_SIZE, 380, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(caseDirBrowseButton))) - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addComponent(caseDirBrowseButton))) + .addContainerGap()) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(singleUserCaseRadioButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(multiUserCaseRadioButton)) + .addComponent(caseParentDirWarningLabel)) + .addGap(0, 0, Short.MAX_VALUE)))) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) @@ -167,7 +317,15 @@ final class NewCaseVisualPanel1 extends JPanel implements DocumentListener { .addComponent(jLabel2) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(caseDirTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addContainerGap(32, Short.MAX_VALUE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(singleUserCaseRadioButton) + .addComponent(multiUserCaseRadioButton)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(caseParentDirWarningLabel) + .addGap(1, 1, 1) + .addComponent(multiUserSettingsWarningLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); }// //GEN-END:initComponents @@ -179,23 +337,27 @@ final class NewCaseVisualPanel1 extends JPanel implements DocumentListener { * @param evt the action event */ private void caseDirBrowseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_caseDirBrowseButtonActionPerformed - // show the directory chooser where the case directory will be created - fc.setDragEnabled(false); + fileChooser.setDragEnabled(false); if (!caseParentDirTextField.getText().trim().equals("")) { - fc.setCurrentDirectory(new File(caseParentDirTextField.getText())); + fileChooser.setCurrentDirectory(new File(caseParentDirTextField.getText())); } - fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); - //fc.setSelectedFile(new File("C:\\Program Files\\")); - //disableTextField(fc); // disable all the text field on the file chooser - - int returnValue = fc.showDialog((Component) evt.getSource(), NbBundle.getMessage(this.getClass(), + fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + int choice = fileChooser.showDialog((Component) evt.getSource(), NbBundle.getMessage(this.getClass(), "NewCaseVisualPanel1.caseDirBrowse.selectButton.text")); - if (returnValue == JFileChooser.APPROVE_OPTION) { - String path = fc.getSelectedFile().getPath(); - caseParentDirTextField.setText(path); // put the path to the textfield + if (JFileChooser.APPROVE_OPTION == choice) { + String path = fileChooser.getSelectedFile().getPath(); + caseParentDirTextField.setText(path); } }//GEN-LAST:event_caseDirBrowseButtonActionPerformed + private void singleUserCaseRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_singleUserCaseRadioButtonActionPerformed + handleUpdate(); + }//GEN-LAST:event_singleUserCaseRadioButtonActionPerformed + + private void multiUserCaseRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_multiUserCaseRadioButtonActionPerformed + handleUpdate(); + }//GEN-LAST:event_multiUserCaseRadioButtonActionPerformed + // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JButton caseDirBrowseButton; private javax.swing.JLabel caseDirLabel; @@ -203,8 +365,13 @@ final class NewCaseVisualPanel1 extends JPanel implements DocumentListener { private javax.swing.JLabel caseNameLabel; private javax.swing.JTextField caseNameTextField; private javax.swing.JTextField caseParentDirTextField; + private javax.swing.JLabel caseParentDirWarningLabel; + private javax.swing.ButtonGroup caseTypeButtonGroup; private javax.swing.JLabel jLabel1; private javax.swing.JLabel jLabel2; + private javax.swing.JRadioButton multiUserCaseRadioButton; + private javax.swing.JLabel multiUserSettingsWarningLabel; + private javax.swing.JRadioButton singleUserCaseRadioButton; // End of variables declaration//GEN-END:variables /** diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardAction.java b/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardAction.java index 0891ce74ed..5e6b5e5ceb 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardAction.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardAction.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2014 Basis Technology Corp. + * Copyright 2011-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,6 +24,8 @@ import java.io.File; import java.text.MessageFormat; import java.util.logging.Level; import javax.swing.JComponent; +import javax.swing.SwingWorker; +import javax.swing.SwingUtilities; import org.openide.DialogDescriptor; import org.openide.DialogDisplayer; import org.openide.NotifyDescriptor; @@ -33,6 +35,12 @@ import org.openide.util.NbBundle; import org.openide.util.actions.CallableSystemAction; import org.openide.util.actions.SystemAction; import org.sleuthkit.autopsy.coreutils.Logger; +import javax.swing.JOptionPane; +import org.sleuthkit.autopsy.casemodule.Case.CaseType; +import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.datamodel.CaseDbConnectionInfo; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskData.DbType; /** * Action to open the New Case wizard. @@ -74,7 +82,7 @@ final class NewCaseWizardAction extends CallableSystemAction { * The method to perform new case creation */ private void newCaseAction() { - WizardDescriptor wizardDescriptor = new WizardDescriptor(getPanels()); + final WizardDescriptor wizardDescriptor = new WizardDescriptor(getPanels()); // {0} will be replaced by WizardDesriptor.Panel.getComponent().getName() wizardDescriptor.setTitleFormat(new MessageFormat("{0}")); wizardDescriptor.setTitle(NbBundle.getMessage(this.getClass(), "NewCaseWizardAction.newCase.windowTitle.text")); @@ -82,32 +90,67 @@ final class NewCaseWizardAction extends CallableSystemAction { dialog.setVisible(true); dialog.toFront(); - boolean finished = wizardDescriptor.getValue() == WizardDescriptor.FINISH_OPTION; // check if it finishes (it's not cancelled) - boolean isCancelled = wizardDescriptor.getValue() == WizardDescriptor.CANCEL_OPTION; // check if the "Cancel" button is pressed + if (wizardDescriptor.getValue() == WizardDescriptor.FINISH_OPTION) { + new SwingWorker() { - // if the finish button is pressed (not cancelled) - if (finished) { - // now start the 'Add Image' wizard - //TODO fix for local - AddImageAction addImageAction = SystemAction.get(AddImageAction.class); - addImageAction.actionPerformed(null); - } + @Override + protected Void doInBackground() throws Exception { + // Create case. - // if Cancel button is pressed - if (isCancelled) { - String createdDirectory = (String) wizardDescriptor.getProperty("createdDirectory"); //NON-NLS - if (createdDirectory != null) { - logger.log(Level.INFO, "Deleting a created case directory due to isCancelled set, dir: " + createdDirectory); //NON-NLS - Case.deleteCaseDirectory(new File(createdDirectory)); - } - // if there's case opened, close the case - if (Case.existsCurrentCase()) { - // close the previous case if there's any - CaseCloseAction closeCase = SystemAction.get(CaseCloseAction.class); - closeCase.actionPerformed(null); - } + String caseNumber = (String) wizardDescriptor.getProperty("caseNumber"); //NON-NLS + String examiner = (String) wizardDescriptor.getProperty("caseExaminer"); //NON-NLS + final String caseName = (String) wizardDescriptor.getProperty("caseName"); //NON-NLS + String createdDirectory = (String) wizardDescriptor.getProperty("createdDirectory"); //NON-NLS + CaseType caseType = CaseType.values()[(int) wizardDescriptor.getProperty("caseType")]; //NON-NLS + + Case.create(createdDirectory, caseName, caseNumber, examiner, caseType); + return null; + } + + @Override + protected void done() { + try { + get(); + CaseType currentCaseType = CaseType.values()[(int) wizardDescriptor.getProperty("caseType")]; //NON-NLS + CaseDbConnectionInfo info = UserPreferences.getDatabaseConnectionInfo(); + if ((currentCaseType == CaseType.SINGLE_USER_CASE) || ((info.getDbType() != DbType.SQLITE) && SleuthkitCase.tryConnectOld(info.getHost(), info.getPort(), info.getUserName(), info.getPassword(), info.getDbType()))) { + AddImageAction addImageAction = SystemAction.get(AddImageAction.class); + addImageAction.actionPerformed(null); + } else { + JOptionPane.showMessageDialog(null, + NbBundle.getMessage(this.getClass(), "NewCaseWizardAction.databaseProblem1.text"), + NbBundle.getMessage(this.getClass(), "NewCaseWizardAction.databaseProblem2.text"), + JOptionPane.ERROR_MESSAGE); + doFailedCaseCleanup(wizardDescriptor); + } + + } catch (Exception ex) { + final String caseName = (String) wizardDescriptor.getProperty("caseName"); //NON-NLS + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog(null, NbBundle.getMessage(this.getClass(), + "CaseCreateAction.msgDlg.cantCreateCase.msg") + " " + caseName, + NbBundle.getMessage(this.getClass(), + "CaseOpenAction.msgDlg.cantOpenCase.title"), + JOptionPane.ERROR_MESSAGE); + }); + doFailedCaseCleanup(wizardDescriptor); + } + } + }.execute(); + } else { + new Thread(() -> { + doFailedCaseCleanup(wizardDescriptor); + }).start(); + } + } + + private void doFailedCaseCleanup(WizardDescriptor wizardDescriptor) { + String createdDirectory = (String) wizardDescriptor.getProperty("createdDirectory"); //NON-NLS + + if (createdDirectory != null) { + logger.log(Level.INFO, "Deleting a created case directory due to an error, dir: {0}", createdDirectory); //NON-NLS + Case.deleteCaseDirectory(new File(createdDirectory)); } - panels = null; // reset the panel } /** @@ -131,7 +174,7 @@ final class NewCaseWizardAction extends CallableSystemAction { if (c instanceof JComponent) { // assume Swing components JComponent jc = (JComponent) c; // Sets step number of a component - jc.putClientProperty("WizardPanel_contentSelectedIndex", new Integer(i)); + jc.putClientProperty("WizardPanel_contentSelectedIndex", i); // Sets steps names for a panel jc.putClientProperty("WizardPanel_contentData", steps); // Turn on subtitle creation on each step diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardPanel1.java b/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardPanel1.java index d368bef209..b7b432a9eb 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardPanel1.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardPanel1.java @@ -33,8 +33,8 @@ import org.openide.DialogDisplayer; import org.openide.NotifyDescriptor; import org.openide.WizardDescriptor; import org.openide.WizardValidationException; -import org.openide.util.Exceptions; import org.openide.util.HelpCtx; +import org.sleuthkit.autopsy.casemodule.Case.CaseType; import org.sleuthkit.autopsy.coreutils.ModuleSettings; /** @@ -170,7 +170,8 @@ class NewCaseWizardPanel1 implements WizardDescriptor.ValidatingPanel sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,14 +21,14 @@ package org.sleuthkit.autopsy.casemodule; import java.util.HashSet; import java.util.Iterator; import java.util.Set; -import javax.swing.SwingUtilities; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import org.openide.WizardDescriptor; import org.openide.WizardValidationException; -import org.openide.util.Exceptions; import org.openide.util.HelpCtx; -import org.openide.util.NbBundle; +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 @@ -48,6 +48,7 @@ class NewCaseWizardPanel2 implements WizardDescriptor.ValidatingPanel it; synchronized (listeners) { - it = new HashSet(listeners).iterator(); + it = new HashSet<>(listeners).iterator(); } ChangeEvent ev = new ChangeEvent(this); while (it.hasNext()) { @@ -154,6 +155,7 @@ class NewCaseWizardPanel2 implements WizardDescriptor.ValidatingPanel sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,8 +24,8 @@ import java.io.File; import java.util.logging.Level; import javax.swing.JOptionPane; import javax.swing.JTable; +import javax.swing.SwingUtilities; import javax.swing.table.AbstractTableModel; - import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; @@ -37,7 +37,7 @@ class OpenRecentCasePanel extends javax.swing.JPanel { static String[] caseNames; static String[] casePaths; - private static Logger logger = Logger.getLogger(OpenRecentCasePanel.class.getName()); + private static final Logger logger = Logger.getLogger(OpenRecentCasePanel.class.getName()); private static OpenRecentCasePanel instance; private RecentCasesTableModel model; @@ -184,8 +184,8 @@ class OpenRecentCasePanel extends javax.swing.JPanel { logger.log(Level.INFO, "No Case paths exist, cannot open the case"); //NON-NLS return; } - String casePath = casePaths[imagesTable.getSelectedRow()]; - String caseName = caseNames[imagesTable.getSelectedRow()]; + final String casePath = casePaths[imagesTable.getSelectedRow()]; + final String caseName = caseNames[imagesTable.getSelectedRow()]; if (!casePath.equals("")) { // Close the startup menu try { @@ -195,27 +195,34 @@ class OpenRecentCasePanel extends javax.swing.JPanel { logger.log(Level.WARNING, "Error: couldn't open case: " + caseName, ex); //NON-NLS } // Open the recent cases - try { - if (caseName.equals("") || casePath.equals("") || (!new File(casePath).exists())) { - JOptionPane.showMessageDialog(null, - NbBundle.getMessage(OpenRecentCasePanel.class, - "OpenRecentCasePanel.openCase.msgDlg.caseDoesntExist.msg", - caseName), - NbBundle.getMessage(OpenRecentCasePanel.class, - "OpenRecentCasePanel.openCase.msgDlg.err"), - JOptionPane.ERROR_MESSAGE); - RecentCases.getInstance().removeRecentCase(caseName, casePath); // remove the recent case if it doesn't exist anymore + if (caseName.equals("") || casePath.equals("") || (!new File(casePath).exists())) { + JOptionPane.showMessageDialog(null, + NbBundle.getMessage(this.getClass(), + "OpenRecentCasePanel.openCase.msgDlg.caseDoesntExist.msg", + caseName), + NbBundle.getMessage(this.getClass(), + "OpenRecentCasePanel.openCase.msgDlg.err"), + JOptionPane.ERROR_MESSAGE); + RecentCases.getInstance().removeRecentCase(caseName, casePath); // remove the recent case if it doesn't exist anymore - //if case is not opened, open the start window - if (Case.isCaseOpen() == false) { - StartupWindowProvider.getInstance().open(); - } - - } else { - Case.open(casePath); // open the case + //if case is not opened, open the start window + if (Case.isCaseOpen() == false) { + StartupWindowProvider.getInstance().open(); } - } catch (CaseActionException ex) { - logger.log(Level.WARNING, "Error: couldn't open case: " + caseName, ex); //NON-NLS + + } else { + new Thread(() -> { + try { + Case.open(casePath); + } catch (CaseActionException ex) { + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog(null, ex.getMessage(), NbBundle.getMessage(this.getClass(), "CaseOpenAction.msgDlg.cantOpenCase.title"), JOptionPane.ERROR_MESSAGE); + if (!Case.isCaseOpen()) { + StartupWindowProvider.getInstance().open(); + } + }); + } + }).start(); } } } @@ -286,7 +293,7 @@ class OpenRecentCasePanel extends javax.swing.JPanel { ret = shortenPath(casePaths[rowIndex]); break; default: - logger.log(Level.SEVERE, "Invalid table column index: " + columnIndex); //NON-NLS + logger.log(Level.SEVERE, "Invalid table column index: {0}", columnIndex); //NON-NLS break; } return ret; diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/RecentItems.java b/Core/src/org/sleuthkit/autopsy/casemodule/RecentItems.java index 5120f98152..a4859c9846 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/RecentItems.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/RecentItems.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2014 Basis Technology Corp. + * Copyright 2011-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,11 +22,10 @@ import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; -import java.util.logging.Level; import javax.swing.JOptionPane; import javax.swing.JPanel; +import javax.swing.SwingUtilities; import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.coreutils.Logger; /** * This class is used to add the action to the recent case menu item. When the @@ -34,8 +33,8 @@ import org.sleuthkit.autopsy.coreutils.Logger; */ class RecentItems implements ActionListener { - String caseName; - String casePath; + final String caseName; + final String casePath; private JPanel caller; // for error handling /** @@ -65,22 +64,25 @@ class RecentItems implements ActionListener { //if case is not opened, open the start window if (Case.isCaseOpen() == false) { - EventQueue.invokeLater(new Runnable() { - - @Override - public void run() { - StartupWindowProvider.getInstance().open(); - } - + EventQueue.invokeLater(() -> { + StartupWindowProvider.getInstance().open(); }); } } else { - try { - Case.open(casePath); // open the case - } catch (CaseActionException ex) { - Logger.getLogger(RecentItems.class.getName()).log(Level.WARNING, "Error: Couldn't open recent case at " + casePath, ex); //NON-NLS - } + new Thread(() -> { + // Create case. + try { + Case.open(casePath); + } catch (CaseActionException ex) { + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog(null, ex.getMessage(), NbBundle.getMessage(RecentItems.this.getClass(), "CaseOpenAction.msgDlg.cantOpenCase.title"), JOptionPane.ERROR_MESSAGE); + if (!Case.isCaseOpen()) { + StartupWindowProvider.getInstance().open(); + } + }); + } + }).start(); } } } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/SingleUserCaseImporter.java b/Core/src/org/sleuthkit/autopsy/casemodule/SingleUserCaseImporter.java new file mode 100644 index 0000000000..b5498cc284 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/SingleUserCaseImporter.java @@ -0,0 +1,1544 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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; + +import java.awt.Dimension; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Date; +import java.util.logging.Level; +import javax.swing.JOptionPane; +import static javax.swing.JOptionPane.OK_CANCEL_OPTION; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.SwingUtilities; +import org.apache.commons.io.FileUtils; +import org.openide.util.NbBundle; +import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.casemodule.Case.CaseType; +import static org.sleuthkit.autopsy.casemodule.Case.MODULE_FOLDER; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.CaseDbConnectionInfo; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.autopsy.coreutils.NetworkUtils; +import org.sleuthkit.autopsy.coreutils.TimeStampUtils; +import org.sleuthkit.autopsy.coreutils.UNCPathUtilities; + +/** + * Import case(s) from single-user to multi-user. Recursively scans subfolders. + */ +public class SingleUserCaseImporter implements Runnable { + + private static final String AUTOPSY_DB_FILE = "autopsy.db"; //NON-NLS + private static final String DOTAUT = ".aut"; //NON-NLS + public static final String CASE_IMPORT_LOG_FILE = "case_import_log.txt"; //NON-NLS + private static final String logDateFormat = "yyyy/MM/dd HH:mm:ss"; //NON-NLS + //If TIMELINE_FOLDER changes, also update TIMELINE in EventsRepository + private static final String TIMELINE_FOLDER = "Timeline"; //NON-NLS + //If TIMELINE_FILE changes, also update TIMELINE_FILE in EventDB + private final static String TIMELINE_FILE = "events.db"; //NON-NLS + private final static String AIM_LOG_FILE_NAME = "auto_ingest_log.txt"; //NON-NLS + private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat(logDateFormat); + private static final int MAX_DB_NAME_LENGTH = 63; + private final String SEP = System.getProperty("line.separator"); + private final Object threadWaitNotifyLock = new Object(); + + private final Path caseInputFolder; + private final String caseOutputFolder; + private final String imageInputFolder; + private final String imageOutputFolder; + private final boolean copySourceImages; + private final boolean deleteCase; + private final CaseDbConnectionInfo db; + private final ImportDoneCallback notifyOnComplete; + private final UNCPathUtilities uncPathUtilities = new UNCPathUtilities(); + private PrintWriter writer; + private XMLCaseManagement oldXmlCaseManagement; + private XMLCaseManagement newXmlCaseManagement; + private boolean addTimestamp; + private int userAnswer = 0; + + /** + * SingleUserCaseImporter constructor + * + * @param caseInput the folder to start our case search from. Will + * find valid cases from this folder down, and + * process them. + * @param caseOutput the folder to place processed cases into + * @param imageInput the folder that holds the images to copy over + * @param imageOutput the destination folder for the images + * @param database the connection information to talk to the + * PostgreSQL db + * @param copySourceImages true if images should be copied + * @param deleteCase true if the old version of the case should be + * deleted after import + * @param addTimestamp true if the output case name should end in a + * timestamp, false otherwise + * @param callback a callback from the calling panel for + * notification when the import has completed. This + * is a Runnable on a different thread. + */ + public SingleUserCaseImporter(String caseInput, String caseOutput, String imageInput, String imageOutput, CaseDbConnectionInfo database, + boolean copySourceImages, boolean deleteCase, ImportDoneCallback callback, boolean addTimestamp) { + this.caseInputFolder = Paths.get(caseInput); + this.caseOutputFolder = caseOutput; + this.imageInputFolder = imageInput; + this.imageOutputFolder = imageOutput; + this.copySourceImages = copySourceImages; + this.deleteCase = deleteCase; + this.db = database; + this.notifyOnComplete = callback; + this.addTimestamp = addTimestamp; + } + + /** + * Tests if the input path has a corresponding image input folder and no + * repeated case names in the path. If both of these conditions are true, we + * can process this case, otherwise we can not. + * + * @param icd the import case data for the current case + * + * @return true if we can process it, false if not + */ + private boolean canProcess(ImportCaseData icd) { + try { + String relativeCaseName = TimeStampUtils.removeTimeStamp(icd.getRelativeCaseName()); + String caseName = TimeStampUtils.removeTimeStamp(icd.getOldCaseName()); + + // check for image folder + Path testImageInputsFromOldCase = Paths.get(imageInputFolder, relativeCaseName); + if (copySourceImages) { + if (!testImageInputsFromOldCase.toFile().isDirectory()) { + log(imageInputFolder + " has no corresponding images folder. Not able to process."); + return false; + } else { + icd.setSpecificImageInputFolder(testImageInputsFromOldCase); + } + + Path imagePath = Paths.get(imageInputFolder); + // see if case name is in the image path. This causes bad things to happen with the parsing. + for (int x = 0; x < imagePath.getNameCount(); ++x) { + if (caseName.toLowerCase().equals(imagePath.getName(x).toString().toLowerCase())) { + log(imagePath.toString() + " has case name \"" + caseName + "\" within path. Not able to process."); + return false; + } + } + } else { + icd.setSpecificImageInputFolder(testImageInputsFromOldCase); + } + } catch (Exception ex) { + log("Error processing " + icd.specificCaseInputFolder.toString() + ": " + ex.getMessage()); + return false; // anything goes wrong, bail. + } + + return true; + } + + /** + * Handles most of the heavy lifting for importing cases from single-user to + * multi-user. Creates new .aut file, moves folders to the right place, + * imports the database, and updates paths within the database. + * + * @param icd the Import Case Data for the current case + * + * @return true if successful, false if not + */ + private boolean processCase(ImportCaseData icd) { + boolean result = true; + try { + log("Importing case " + icd.getSpecificCaseInputFolder().toString() + " to " + caseOutputFolder + "\\" + icd.getOldCaseName()); //NON-NLS + + checkInputDatabase(icd.getSpecificCaseInputFolder()); + + oldXmlCaseManagement = new XMLCaseManagement(); + + // read old xml config + oldXmlCaseManagement.open(icd.getSpecificCaseInputFolder().resolve(TimeStampUtils.removeTimeStamp(icd.getOldCaseName()) + DOTAUT).toString()); + if (oldXmlCaseManagement.getCaseType() == CaseType.MULTI_USER_CASE) { + throw new Exception(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.AlreadyMultiUser")); + } + + prepareOutput(icd); + + // create sanitized names for database and solr + String caseName = TimeStampUtils.removeTimeStamp(icd.getNewCaseName()); // caseName holds the deconflicted, timestampless name of the case + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss"); //NON-NLS + Date date = new Date(); + String santizedDatabaseName = Case.sanitizeCaseName(caseName); + String dbName = santizedDatabaseName + "_" + dateFormat.format(date); //NON-NLS + String solrName = dbName; + + icd.setSpecificImageOutputFolder(Paths.get(imageOutputFolder, icd.getNewCaseName())); + copyResults(icd); // Copy items to new hostname folder structure + dbName = importDb(dbName, icd.getSpecificCaseInputFolder(), icd.getSpecificCaseOutputFolder().toString()); // Change from SQLite to PostgreSQL + + fixPaths(icd, dbName); // Update paths in DB + + copyImages(icd); // Copy images over + + // create new XML config + newXmlCaseManagement = new XMLCaseManagement(); + newXmlCaseManagement.create(icd.getSpecificCaseOutputFolder().toString(), + caseName, + oldXmlCaseManagement.getCaseExaminer(), + oldXmlCaseManagement.getCaseNumber(), + CaseType.MULTI_USER_CASE, dbName, solrName); + + // Set created date. This calls writefile, no need to call it again + newXmlCaseManagement.setCreatedDate(oldXmlCaseManagement.getCreatedDate()); + + // At this point the import has been finished successfully so we can delete the original case + // (if requested). This *should* be fairly safe - at this point we know there was an autopsy file + // and database in the given directory so the user shouldn't be able to accidently blow away + // their C drive. + if (deleteCase) { + log(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.DeletingCase") + " " + icd.getSpecificCaseInputFolder().toString()); + FileUtils.deleteDirectory(icd.getSpecificCaseInputFolder().toFile()); + } + + result = reportLostImages(db, dbName); + + log("Finished importing case " + icd.getSpecificCaseInputFolder().toString() + " to " + icd.getSpecificCaseOutputFolder().toString()); + } catch (Exception exp) { + /// clean up here + log("Error processing " + icd.specificCaseInputFolder.toString() + ": " + exp.getMessage()); + result = false; + } + return result; + } + + /** + * Searches for images in the filesystem. It parses the new PostgreSQL + * database to find images that should exist, and notifies when they do not. + * + * @param db database credentials + * @param dbName the name of the database + * + * @return true if successfully found all images, false otherwise. + */ + private boolean reportLostImages(CaseDbConnectionInfo db, String dbName) { + boolean result = true; + if (copySourceImages) { + try { + Class.forName("org.postgresql.Driver"); //NON-NLS + Connection dbConnection = DriverManager.getConnection("jdbc:postgresql://" + db.getHost() + ":" + db.getPort() + "/" + dbName, db.getUserName(), db.getPassword()); //NON-NLS + Statement inputStatement = dbConnection.createStatement(); + ResultSet inputResultSet = inputStatement.executeQuery("SELECT * FROM tsk_image_names"); //NON-NLS + + while (inputResultSet.next()) { + + File theFile = new File(inputResultSet.getString(2)); + if (false == theFile.exists()) { + log("Unable to find image " + theFile.toString() + " for case " + dbName); + result = false; + } + } + } catch (Exception ex) { + log("Error. Unable to verify images were copied."); + result = false; + } + } + return result; + } + + /** + * Figure out the input folder for images and return it. + * + * @param icd the import case data for the current case + * + * @return the name of the proper input folder + */ + private File findInputFolder(ImportCaseData icd) { + + File thePath = icd.getSpecificImageInputFolder().resolve(icd.getOldCaseName()).toFile(); + if (thePath.isDirectory()) { + /// we've found it + return thePath; + } else { + return icd.getSpecificImageInputFolder().toFile(); + } + } + + /** + * Ensure the input source has an autopsy.db and exists. + * + * @param caseInput The folder containing a case to import. + * + * @throws Exception + */ + private void checkInputDatabase(Path caseInput) throws Exception { + Path path = caseInput.resolve(AUTOPSY_DB_FILE); + if (false == path.toFile().exists()) { + throw new Exception(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.BadDatabaseFileName")); + } + } + + /** + * Handles case folder, PosgreSql database, and Solr core name deconfliction + * Sets the appropriate portions of the ImportCaseData object. + * + * @param icd the case data folder name + * + * @throws Exception + */ + private void prepareOutput(ImportCaseData icd) throws Exception { + // test for uniqueness + String caseName = icd.getOldCaseName(); + File specificOutputFolder = Paths.get(caseOutputFolder, caseName).toFile(); + String sanitizedCaseName = caseName; + if (specificOutputFolder.exists()) { + // not unique. add numbers before timestamp to specific case name + String timeStamp = TimeStampUtils.getTimeStampOnly(caseName); //NON-NLS + sanitizedCaseName = TimeStampUtils.removeTimeStamp(caseName); + + int number = 1; + String temp = ""; //NON-NLS + while (specificOutputFolder.exists()) { + if (number == Integer.MAX_VALUE) { + // oops. it never became unique. give up. + throw new Exception(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.NonUniqueOutputFolder") + sanitizedCaseName); + } + temp = sanitizedCaseName + "_" + Integer.toString(number) + timeStamp; //NON-NLS + specificOutputFolder = Paths.get(caseOutputFolder, temp).toFile(); + ++number; + } + sanitizedCaseName = temp; + } + + if (addTimestamp && !TimeStampUtils.endsWithTimeStamp(sanitizedCaseName)) { + sanitizedCaseName += "_" + TimeStampUtils.createTimeStamp(); + } + + Path caseOutputPath = Paths.get(caseOutputFolder, sanitizedCaseName); + icd.setNewCaseName(sanitizedCaseName); + icd.setSpecificCaseOutputFolder(caseOutputPath); + caseOutputPath.toFile().mkdirs(); // create output folders just in case + } + + /** + * Copy all the folders at the base level to the new scheme involving + * hostname. Also take care of a few files such as logs, timeline db, etc. + * + * @param icd the case data + * + * @throws IOException + */ + private void copyResults(ImportCaseData icd) throws IOException { + /// get hostname + String hostName = NetworkUtils.getLocalHostName(); + Path destination; + Path source; + + source = icd.getSpecificCaseInputFolder(); + if (source.toFile().exists()) { + destination = icd.getSpecificCaseOutputFolder().resolve(hostName); + FileUtils.copyDirectory(source.toFile(), destination.toFile()); + } + + source = icd.getSpecificCaseInputFolder().resolve(TIMELINE_FILE); + if (source.toFile().exists()) { + destination = Paths.get(icd.getSpecificCaseOutputFolder().toString(), hostName, MODULE_FOLDER, TIMELINE_FOLDER, TIMELINE_FILE); + FileUtils.copyFile(source.toFile(), destination.toFile()); + } + + source = icd.getSpecificCaseInputFolder().resolve(AIM_LOG_FILE_NAME); + destination = icd.getSpecificCaseOutputFolder().resolve(AIM_LOG_FILE_NAME); + if (source.toFile().exists()) { + FileUtils.copyFile(source.toFile(), destination.toFile()); + + } + try (PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(destination.toString(), true)))) { + out.println(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.ImportedAsMultiUser") + new Date()); + } catch (IOException e) { + // if unable to log it, no problem + } + + // Remove the single-user .aut file, database, Timeline database and log + File oldAutopsyFile = Paths.get(icd.getSpecificCaseOutputFolder().toString(), hostName, TimeStampUtils.removeTimeStamp(icd.getOldCaseName()) + DOTAUT).toFile(); + if (oldAutopsyFile.exists()) { + oldAutopsyFile.delete(); + } + + File oldDatabaseFile = Paths.get(icd.getSpecificCaseOutputFolder().toString(), hostName, AUTOPSY_DB_FILE).toFile(); + if (oldDatabaseFile.exists()) { + oldDatabaseFile.delete(); + } + + File oldTimelineFile = Paths.get(icd.getSpecificCaseOutputFolder().toString(), hostName, TIMELINE_FILE).toFile(); + if (oldTimelineFile.exists()) { + oldTimelineFile.delete(); + } + + File oldIngestLog = Paths.get(icd.getSpecificCaseOutputFolder().toString(), hostName, AIM_LOG_FILE_NAME).toFile(); + if (oldIngestLog.exists()) { + oldIngestLog.delete(); + } + } + + /** + * Import the database from SQLite to PostgreSQL. Do not change any of the + * data while loading it over. Fixing paths is done once the database is + * completely imported. + * + * @param dbName the name of the database, could have name collision + * @param inputPath the path to the input case + * @param outputCaseName the name of the output case, could have extra + * digits to avoid name collisions + * + * @return the deconflicted name of the PostgreSQL database that was created + * + * @throws SQLException + * @throws ClassNotFoundException + */ + private String importDb(String dbName, Path inputPath, String outputCaseName) throws SQLException, ClassNotFoundException, Exception { + // deconflict the database name + dbName = deconflictDatabaseName(db, dbName); + + // Create a new database via SleuthkitCase + SleuthkitCase newCase = SleuthkitCase.newCase(dbName, db, outputCaseName); + newCase.close(); + + /// Migrate from SQLite to PostgreSQL + Class.forName("org.sqlite.JDBC"); //NON-NLS + Connection sqliteConnection = DriverManager.getConnection("jdbc:sqlite:" + inputPath.resolve(AUTOPSY_DB_FILE).toString(), "", ""); //NON-NLS + + Connection postgresqlConnection = DriverManager.getConnection("jdbc:postgresql://" + db.getHost() + ":" + db.getPort() + "/" + dbName, db.getUserName(), db.getPassword()); //NON-NLS + + // blackboard_artifact_types + Statement inputStatement = sqliteConnection.createStatement(); + ResultSet inputResultSet = inputStatement.executeQuery("SELECT * FROM blackboard_artifact_types"); //NON-NLS + Statement outputStatement; + Statement numberingPK; + long biggestPK = 0; + + while (inputResultSet.next()) { + try { + long value = inputResultSet.getLong(1); + if (value > biggestPK) { + biggestPK = value; + } + Statement check = postgresqlConnection.createStatement(); + ResultSet checkResult = check.executeQuery("SELECT * FROM blackboard_artifact_types WHERE artifact_type_id=" + value + " AND type_name LIKE '" + inputResultSet.getString(2) + "' AND display_name LIKE '" + inputResultSet.getString(3) + "'"); //NON-NLS + if (!checkResult.isBeforeFirst()) { // only insert if it doesn't exist + String sql = "INSERT INTO blackboard_artifact_types (artifact_type_id, type_name, display_name) VALUES (" + + value + ", '" + + SleuthkitCase.escapeSingleQuotes(inputResultSet.getString(2)) + "'," + + " ? )"; //NON-NLS + PreparedStatement pst = postgresqlConnection.prepareStatement(sql); + populateNullableString(pst, inputResultSet, 3, 1); + pst.executeUpdate(); + } + } catch (SQLException ex) { + if (ex.getErrorCode() != 0) { // 0 if the entry already exists + throw new SQLException(ex); + } + } + } + numberingPK = postgresqlConnection.createStatement(); + numberingPK.execute("ALTER SEQUENCE blackboard_artifact_types_artifact_type_id_seq RESTART WITH " + (biggestPK + 1)); //NON-NLS + + // blackboard_attribute_types + biggestPK = 0; + inputStatement = sqliteConnection.createStatement(); + inputResultSet = inputStatement.executeQuery("SELECT * FROM blackboard_attribute_types"); //NON-NLS + + while (inputResultSet.next()) { + try { + long value = inputResultSet.getLong(1); + if (value > biggestPK) { + biggestPK = value; + } + Statement check = postgresqlConnection.createStatement(); + ResultSet checkResult = check.executeQuery("SELECT * FROM blackboard_attribute_types WHERE attribute_type_id=" + value + " AND type_name LIKE '" + inputResultSet.getString(2) + "' AND display_name LIKE '" + inputResultSet.getString(3) + "'"); //NON-NLS + if (!checkResult.isBeforeFirst()) { // only insert if it doesn't exist + String sql = "INSERT INTO blackboard_attribute_types (attribute_type_id, type_name, display_name) VALUES (" + + value + ", '" + + SleuthkitCase.escapeSingleQuotes(inputResultSet.getString(2)) + "'," + + " ? )"; //NON-NLS + + PreparedStatement pst = postgresqlConnection.prepareStatement(sql); + populateNullableString(pst, inputResultSet, 3, 1); + pst.executeUpdate(); + } + } catch (SQLException ex) { + if (ex.getErrorCode() != 0) { // 0 if the entry already exists + throw new SQLException(ex); + } + } + } + numberingPK = postgresqlConnection.createStatement(); + numberingPK.execute("ALTER SEQUENCE blackboard_attribute_types_attribute_type_id_seq RESTART WITH " + (biggestPK + 1)); //NON-NLS + + // tsk_objects + biggestPK = 0; + inputStatement = sqliteConnection.createStatement(); + inputResultSet = inputStatement.executeQuery("SELECT * FROM tsk_objects"); //NON-NLS + + while (inputResultSet.next()) { + outputStatement = postgresqlConnection.createStatement(); + try { + long value = inputResultSet.getLong(1); + if (value > biggestPK) { + biggestPK = value; + } + outputStatement.executeUpdate("INSERT INTO tsk_objects (obj_id, par_obj_id, type) VALUES (" + + value + "," + + getNullableLong(inputResultSet, 2) + "," + + inputResultSet.getInt(3) + ")"); //NON-NLS + } catch (SQLException ex) { + if (ex.getErrorCode() != 0) { // 0 if the entry already exists + throw new SQLException(ex); + } + } + } + numberingPK = postgresqlConnection.createStatement(); + numberingPK.execute("ALTER SEQUENCE tsk_objects_obj_id_seq RESTART WITH " + (biggestPK + 1)); //NON-NLS + + // tsk_image_names, no primary key + inputStatement = sqliteConnection.createStatement(); + inputResultSet = inputStatement.executeQuery("SELECT * FROM tsk_image_names"); //NON-NLS + + while (inputResultSet.next()) { + outputStatement = postgresqlConnection.createStatement(); + try { + outputStatement.executeUpdate("INSERT INTO tsk_image_names (obj_id, name, sequence) VALUES (" + + inputResultSet.getLong(1) + ",'" + + inputResultSet.getString(2) + "'," + + inputResultSet.getInt(3) + ")"); //NON-NLS + } catch (SQLException ex) { + if (ex.getErrorCode() != 0) { // 0 if the entry already exists + throw new SQLException(ex); + } + } + } + + // tsk_image_info + biggestPK = 0; + inputStatement = sqliteConnection.createStatement(); + inputResultSet = inputStatement.executeQuery("SELECT * FROM tsk_image_info"); //NON-NLS + + while (inputResultSet.next()) { + outputStatement = postgresqlConnection.createStatement(); + try { + long value = inputResultSet.getLong(1); + if (value > biggestPK) { + biggestPK = value; + } + String sql = "INSERT INTO tsk_image_info (obj_id, type, ssize, tzone, size, md5, display_name) VALUES (" + + value + "," + + getNullableInt(inputResultSet, 2) + "," + + getNullableInt(inputResultSet, 3) + "," + + " ? ," + + getNullableLong(inputResultSet, 5) + "," + + " ? ," + + " ? )"; //NON-NLS + + PreparedStatement pst = postgresqlConnection.prepareStatement(sql); + populateNullableString(pst, inputResultSet, 4, 1); + populateNullableString(pst, inputResultSet, 6, 2); + populateNullableString(pst, inputResultSet, 7, 3); + pst.executeUpdate(); + + } catch (SQLException ex) { + if (ex.getErrorCode() != 0) { // 0 if the entry already exists + throw new SQLException(ex); + } + } + } + numberingPK = postgresqlConnection.createStatement(); + numberingPK.execute("ALTER SEQUENCE tsk_image_info_obj_id_seq RESTART WITH " + (biggestPK + 1)); //NON-NLS + + // tsk_fs_info + biggestPK = 0; + inputStatement = sqliteConnection.createStatement(); + inputResultSet = inputStatement.executeQuery("SELECT * FROM tsk_fs_info"); //NON-NLS + + while (inputResultSet.next()) { + try { + long value = inputResultSet.getLong(1); + if (value > biggestPK) { + biggestPK = value; + } + String sql = "INSERT INTO tsk_fs_info (obj_id, img_offset, fs_type, block_size, block_count, root_inum, first_inum, last_inum, display_name) VALUES (" + + value + "," + + inputResultSet.getLong(2) + "," + + inputResultSet.getInt(3) + "," + + inputResultSet.getLong(4) + "," + + inputResultSet.getLong(5) + "," + + inputResultSet.getLong(6) + "," + + inputResultSet.getLong(7) + "," + + inputResultSet.getLong(8) + "," + + " ? )"; //NON-NLS + + PreparedStatement pst = postgresqlConnection.prepareStatement(sql); + populateNullableString(pst, inputResultSet, 9, 1); + pst.executeUpdate(); + + } catch (SQLException ex) { + if (ex.getErrorCode() != 0) { // 0 if the entry already exists + throw new SQLException(ex); + } + } + } + numberingPK = postgresqlConnection.createStatement(); + numberingPK.execute("ALTER SEQUENCE tsk_fs_info_obj_id_seq RESTART WITH " + (biggestPK + 1)); //NON-NLS + + // tsk_files_path + biggestPK = 0; + inputStatement = sqliteConnection.createStatement(); + inputResultSet = inputStatement.executeQuery("SELECT * FROM tsk_files_path"); //NON-NLS + + while (inputResultSet.next()) { + outputStatement = postgresqlConnection.createStatement(); + try { + long value = inputResultSet.getLong(1); + if (value > biggestPK) { + biggestPK = value; + } + outputStatement.executeUpdate("INSERT INTO tsk_files_path (obj_id, path) VALUES (" + + value + ", '" + + SleuthkitCase.escapeSingleQuotes(inputResultSet.getString(2)) + "')"); //NON-NLS + } catch (SQLException ex) { + if (ex.getErrorCode() != 0) { // 0 if the entry already exists + throw new SQLException(ex); + } + } + } + numberingPK = postgresqlConnection.createStatement(); + numberingPK.execute("ALTER SEQUENCE tsk_files_path_obj_id_seq RESTART WITH " + (biggestPK + 1)); //NON-NLS + + // tsk_files + biggestPK = 0; + inputStatement = sqliteConnection.createStatement(); + inputResultSet = inputStatement.executeQuery("SELECT * FROM tsk_files"); //NON-NLS + + while (inputResultSet.next()) { + try { + long value = inputResultSet.getLong(1); + if (value > biggestPK) { + biggestPK = value; + } + String sql = "INSERT INTO tsk_files (obj_id, fs_obj_id, attr_type, attr_id, name, meta_addr, meta_seq, type, has_layout, has_path, dir_type, meta_type, dir_flags, meta_flags, size, ctime, crtime, atime, mtime, mode, uid, gid, md5, known, parent_path) VALUES (" + + value + "," + + getNullableLong(inputResultSet, 2) + "," + + getNullableInt(inputResultSet, 3) + "," + + getNullableInt(inputResultSet, 4) + ",'" + + SleuthkitCase.escapeSingleQuotes(inputResultSet.getString(5)) + "'," + + getNullableLong(inputResultSet, 6) + "," + + getNullableLong(inputResultSet, 7) + "," + + getNullableInt(inputResultSet, 8) + "," + + getNullableInt(inputResultSet, 9) + "," + + getNullableInt(inputResultSet, 10) + "," + + getNullableInt(inputResultSet, 11) + "," + + getNullableInt(inputResultSet, 12) + "," + + getNullableInt(inputResultSet, 13) + "," + + getNullableInt(inputResultSet, 14) + "," + + getNullableLong(inputResultSet, 15) + "," + + getNullableLong(inputResultSet, 16) + "," + + getNullableLong(inputResultSet, 17) + "," + + getNullableLong(inputResultSet, 18) + "," + + getNullableLong(inputResultSet, 19) + "," + + getNullableInt(inputResultSet, 20) + "," + + getNullableInt(inputResultSet, 21) + "," + + getNullableInt(inputResultSet, 22) + "," + + " ? ," + + getNullableInt(inputResultSet, 24) + "," + + " ? )"; //NON-NLS + + PreparedStatement pst = postgresqlConnection.prepareStatement(sql); + populateNullableString(pst, inputResultSet, 23, 1); + populateNullableString(pst, inputResultSet, 25, 2); + pst.executeUpdate(); + + } catch (SQLException ex) { + if (ex.getErrorCode() != 0) { // 0 if the entry already exists + throw new SQLException(ex); + } + } + } + numberingPK = postgresqlConnection.createStatement(); + numberingPK.execute("ALTER SEQUENCE tsk_files_obj_id_seq RESTART WITH " + (biggestPK + 1)); //NON-NLS + + // tsk_file_layout, no primary key + inputStatement = sqliteConnection.createStatement(); + inputResultSet = inputStatement.executeQuery("SELECT * FROM tsk_file_layout"); //NON-NLS + + while (inputResultSet.next()) { + outputStatement = postgresqlConnection.createStatement(); + try { + outputStatement.executeUpdate("INSERT INTO tsk_file_layout (obj_id, byte_start, byte_len, sequence) VALUES (" + + inputResultSet.getLong(1) + "," + + inputResultSet.getLong(2) + "," + + inputResultSet.getLong(3) + "," + + inputResultSet.getInt(4) + ")"); //NON-NLS + } catch (SQLException ex) { + if (ex.getErrorCode() != 0) { // 0 if the entry already exists + throw new SQLException(ex); + } + } + } + + // tsk_db_info, no primary key + inputStatement = sqliteConnection.createStatement(); + inputResultSet = inputStatement.executeQuery("SELECT * FROM tsk_db_info"); //NON-NLS + + while (inputResultSet.next()) { + outputStatement = postgresqlConnection.createStatement(); + try { + Statement check = postgresqlConnection.createStatement(); + ResultSet checkResult = check.executeQuery("SELECT * FROM tsk_db_info WHERE schema_ver=" + inputResultSet.getInt(1) + " AND tsk_ver=" + inputResultSet.getInt(2)); //NON-NLS + if (!checkResult.isBeforeFirst()) { // only insert if it doesn't exist + outputStatement.executeUpdate("INSERT INTO tsk_db_info (schema_ver, tsk_ver) VALUES (" + + getNullableInt(inputResultSet, 1) + "," + + getNullableInt(inputResultSet, 2) + ")"); //NON-NLS + } + } catch (SQLException ex) { + if (ex.getErrorCode() != 0) { // 0 if the entry already exists + throw new SQLException(ex); + } + } + } + + // tag_names + biggestPK = 0; + inputStatement = sqliteConnection.createStatement(); + inputResultSet = inputStatement.executeQuery("SELECT * FROM tag_names"); //NON-NLS + + while (inputResultSet.next()) { + try { + long value = inputResultSet.getLong(1); + if (value > biggestPK) { + biggestPK = value; + } + String sql = "INSERT INTO tag_names (tag_name_id, display_name, description, color) VALUES (" + + value + "," + + " ? ,'" + + SleuthkitCase.escapeSingleQuotes(inputResultSet.getString(3)) + "','" + + SleuthkitCase.escapeSingleQuotes(inputResultSet.getString(4)) + "')"; //NON-NLS + + PreparedStatement pst = postgresqlConnection.prepareStatement(sql); + populateNullableString(pst, inputResultSet, 2, 1); + pst.executeUpdate(); + + } catch (SQLException ex) { + if (ex.getErrorCode() != 0) { // 0 if the entry already exists + throw new SQLException(ex); + } + } + } + numberingPK = postgresqlConnection.createStatement(); + numberingPK.execute("ALTER SEQUENCE tag_names_tag_name_id_seq RESTART WITH " + (biggestPK + 1)); //NON-NLS + + // reports + biggestPK = 0; + inputStatement = sqliteConnection.createStatement(); + inputResultSet = inputStatement.executeQuery("SELECT * FROM reports"); //NON-NLS + + while (inputResultSet.next()) { + outputStatement = postgresqlConnection.createStatement(); + try { + long value = inputResultSet.getLong(1); + if (value > biggestPK) { + biggestPK = value; + } + outputStatement.executeUpdate("INSERT INTO reports (report_id, path, crtime, src_module_name, report_name) VALUES (" + + value + ", '" + + SleuthkitCase.escapeSingleQuotes(inputResultSet.getString(2)) + "'," + + inputResultSet.getInt(3) + ",'" + + SleuthkitCase.escapeSingleQuotes(inputResultSet.getString(4)) + "','" + + SleuthkitCase.escapeSingleQuotes(inputResultSet.getString(5)) + "')"); //NON-NLS + + } catch (SQLException ex) { + if (ex.getErrorCode() != 0) { // 0 if the entry already exists + throw new SQLException(ex); + } + } + } + numberingPK = postgresqlConnection.createStatement(); + numberingPK.execute("ALTER SEQUENCE reports_report_id_seq RESTART WITH " + (biggestPK + 1)); //NON-NLS + + // blackboard_artifacts + biggestPK = Long.MIN_VALUE; // This table uses very large negative primary key values, so start at Long.MIN_VALUE + inputStatement = sqliteConnection.createStatement(); + inputResultSet = inputStatement.executeQuery("SELECT * FROM blackboard_artifacts"); //NON-NLS + + while (inputResultSet.next()) { + outputStatement = postgresqlConnection.createStatement(); + try { + long value = inputResultSet.getLong(1); + if (value > biggestPK) { + biggestPK = value; + } + outputStatement.executeUpdate("INSERT INTO blackboard_artifacts (artifact_id, obj_id, artifact_type_id) VALUES (" + + value + "," + + inputResultSet.getLong(2) + "," + + inputResultSet.getLong(3) + ")"); //NON-NLS + + } catch (SQLException ex) { + if (ex.getErrorCode() != 0) { // 0 if the entry already exists + throw new SQLException(ex); + } + } + } + numberingPK = postgresqlConnection.createStatement(); + numberingPK.execute("ALTER SEQUENCE blackboard_artifacts_artifact_id_seq RESTART WITH " + (biggestPK + 1)); //NON-NLS + + // blackboard_attributes, no primary key + inputStatement = sqliteConnection.createStatement(); + inputResultSet = inputStatement.executeQuery("SELECT * FROM blackboard_attributes"); //NON-NLS + + while (inputResultSet.next()) { + try { + String sql = "INSERT INTO blackboard_attributes (artifact_id, artifact_type_id, source, context, attribute_type_id, value_type, value_byte, value_text, value_int32, value_int64, value_double) VALUES (" + + inputResultSet.getLong(1) + "," + + inputResultSet.getLong(2) + "," + + " ? ," + + " ? ," + + inputResultSet.getLong(5) + "," + + inputResultSet.getInt(6) + "," + + " ? ," + + " ? ," + + getNullableInt(inputResultSet, 9) + "," + + getNullableLong(inputResultSet, 10) + "," + + " ? )"; //NON-NLS + PreparedStatement pst = postgresqlConnection.prepareStatement(sql); + populateNullableString(pst, inputResultSet, 3, 1); + populateNullableString(pst, inputResultSet, 4, 2); + populateNullableByteArray(pst, inputResultSet, 7, 3); + populateNullableString(pst, inputResultSet, 8, 4); + populateNullableNumeric(pst, inputResultSet, 11, 5); + pst.executeUpdate(); + + } catch (SQLException ex) { + if (ex.getErrorCode() != 0) { // 0 if the entry already exists + throw new SQLException(ex); + } + } + } + + // tsk_vs_parts + biggestPK = 0; + inputStatement = sqliteConnection.createStatement(); + inputResultSet = inputStatement.executeQuery("SELECT * FROM tsk_vs_parts"); //NON-NLS + + while (inputResultSet.next()) { + try { + long value = inputResultSet.getLong(1); + if (value > biggestPK) { + biggestPK = value; + } + String sql = "INSERT INTO tsk_vs_parts (obj_id, addr, start, length, descr, flags) VALUES (" + + value + "," + + inputResultSet.getLong(2) + "," + + inputResultSet.getLong(3) + "," + + inputResultSet.getLong(4) + "," + + " ? ," + + inputResultSet.getInt(6) + ")"; //NON-NLS + PreparedStatement pst = postgresqlConnection.prepareStatement(sql); + populateNullableString(pst, inputResultSet, 5, 1); + pst.executeUpdate(); + + } catch (SQLException ex) { + if (ex.getErrorCode() != 0) { // 0 if the entry already exists + throw new SQLException(ex); + } + } + } + numberingPK = postgresqlConnection.createStatement(); + numberingPK.execute("ALTER SEQUENCE tsk_vs_parts_obj_id_seq RESTART WITH " + (biggestPK + 1)); //NON-NLS + + // tsk_vs_info + biggestPK = 0; + inputStatement = sqliteConnection.createStatement(); + inputResultSet = inputStatement.executeQuery("SELECT * FROM tsk_vs_info"); //NON-NLS + + while (inputResultSet.next()) { + outputStatement = postgresqlConnection.createStatement(); + try { + long value = inputResultSet.getLong(1); + if (value > biggestPK) { + biggestPK = value; + } + outputStatement.executeUpdate("INSERT INTO tsk_vs_info (obj_id, vs_type, img_offset, block_size) VALUES (" + + value + "," + + inputResultSet.getInt(2) + "," + + inputResultSet.getLong(3) + "," + + inputResultSet.getLong(4) + ")"); //NON-NLS + + } catch (SQLException ex) { + if (ex.getErrorCode() != 0) { // 0 if the entry already exists + throw new SQLException(ex); + } + } + } + numberingPK = postgresqlConnection.createStatement(); + numberingPK.execute("ALTER SEQUENCE tsk_vs_info_obj_id_seq RESTART WITH " + (biggestPK + 1)); //NON-NLS + + // tsk_files_derived + biggestPK = 0; + inputStatement = sqliteConnection.createStatement(); + inputResultSet = inputStatement.executeQuery("SELECT * FROM tsk_files_derived"); //NON-NLS + + while (inputResultSet.next()) { + try { + long value = inputResultSet.getLong(1); + if (value > biggestPK) { + biggestPK = value; + } + String sql = "INSERT INTO tsk_files_derived (obj_id, derived_id, rederive) VALUES (" + + value + "," + + inputResultSet.getLong(2) + "," + + " ? )"; //NON-NLS + PreparedStatement pst = postgresqlConnection.prepareStatement(sql); + populateNullableString(pst, inputResultSet, 3, 1); + pst.executeUpdate(); + + } catch (SQLException ex) { + if (ex.getErrorCode() != 0) { // 0 if the entry already exists + throw new SQLException(ex); + } + } + } + numberingPK = postgresqlConnection.createStatement(); + numberingPK.execute("ALTER SEQUENCE tsk_files_derived_obj_id_seq RESTART WITH " + (biggestPK + 1)); //NON-NLS + + // tsk_files_derived_method + biggestPK = 0; + inputStatement = sqliteConnection.createStatement(); + inputResultSet = inputStatement.executeQuery("SELECT * FROM tsk_files_derived_method"); //NON-NLS + + while (inputResultSet.next()) { + try { + long value = inputResultSet.getLong(1); + if (value > biggestPK) { + biggestPK = value; + } + String sql = "INSERT INTO tsk_files_derived_method (derived_id, tool_name, tool_version, other) VALUES (" + + value + ", '" + + inputResultSet.getString(2) + "','" + + inputResultSet.getString(3) + "'," + + " ? )"; //NON-NLS + PreparedStatement pst = postgresqlConnection.prepareStatement(sql); + populateNullableString(pst, inputResultSet, 4, 1); + pst.executeUpdate(); + + } catch (SQLException ex) { + if (ex.getErrorCode() != 0) { // 0 if the entry already exists + throw new SQLException(ex); + } + } + } + numberingPK = postgresqlConnection.createStatement(); + numberingPK.execute("ALTER SEQUENCE tsk_files_derived_method_derived_id_seq RESTART WITH " + (biggestPK + 1)); //NON-NLS + + // content_tags + biggestPK = 0; + inputStatement = sqliteConnection.createStatement(); + inputResultSet = inputStatement.executeQuery("SELECT * FROM content_tags"); //NON-NLS + + while (inputResultSet.next()) { + outputStatement = postgresqlConnection.createStatement(); + try { + long value = inputResultSet.getLong(1); + if (value > biggestPK) { + biggestPK = value; + } + outputStatement.executeUpdate("INSERT INTO content_tags (tag_id, obj_id, tag_name_id, comment, begin_byte_offset, end_byte_offset) VALUES (" + + value + "," + + inputResultSet.getLong(2) + "," + + inputResultSet.getLong(3) + ",'" + + inputResultSet.getString(4) + "'," + + inputResultSet.getLong(5) + "," + + inputResultSet.getLong(6) + ")"); //NON-NLS + + } catch (SQLException ex) { + if (ex.getErrorCode() != 0) { // 0 if the entry already exists + throw new SQLException(ex); + } + } + } + numberingPK = postgresqlConnection.createStatement(); + numberingPK.execute("ALTER SEQUENCE content_tags_tag_id_seq RESTART WITH " + (biggestPK + 1)); //NON-NLS + + // blackboard_artifact_tags + biggestPK = 0; + inputStatement = sqliteConnection.createStatement(); + inputResultSet = inputStatement.executeQuery("SELECT * FROM blackboard_artifact_tags"); //NON-NLS + + while (inputResultSet.next()) { + outputStatement = postgresqlConnection.createStatement(); + try { + long value = inputResultSet.getLong(1); + if (value > biggestPK) { + biggestPK = value; + } + outputStatement.executeUpdate("INSERT INTO blackboard_artifact_tags (tag_id, artifact_id, tag_name_id, comment) VALUES (" + + value + "," + + inputResultSet.getLong(2) + "," + + inputResultSet.getLong(3) + ",'" + + inputResultSet.getString(4) + "')"); //NON-NLS + + } catch (SQLException ex) { + if (ex.getErrorCode() != 0) { // 0 if the entry already exists + throw new SQLException(ex); + } + } + } + numberingPK = postgresqlConnection.createStatement(); + numberingPK.execute("ALTER SEQUENCE blackboard_artifact_tags_tag_id_seq RESTART WITH " + (biggestPK + 1)); //NON-NLS + + sqliteConnection.close(); + postgresqlConnection.close(); + + return dbName; + } + + /** + * Checks that our database name is unique. If it is not, attempts to add + * numbers to it until it is unique. Gives up if it goes through all + * positive integers without finding a unique name. + * + * @param db Database credentials + * @param baseDbName proposed name of the database to check for collisions + * + * @return name to use for the new database. Could be the name passed in. + * + * @throws ClassNotFoundException + * @throws SQLException + * @throws Exception + */ + private String deconflictDatabaseName(CaseDbConnectionInfo db, String baseDbName) throws ClassNotFoundException, SQLException, Exception { + + Class.forName("org.postgresql.Driver"); //NON-NLS + Connection dbNameConnection = DriverManager.getConnection("jdbc:postgresql://" + db.getHost() + ":" + db.getPort() + "/postgres", db.getUserName(), db.getPassword()); //NON-NLS + + int number = 1; + boolean unique = false; + String sanitizedDbName = baseDbName; + if (sanitizedDbName.length() > MAX_DB_NAME_LENGTH) { + sanitizedDbName = sanitizedDbName.substring(0, MAX_DB_NAME_LENGTH); + } + + if (dbNameConnection != null) { + while (unique == false) { + Statement st = dbNameConnection.createStatement(); + ResultSet answer = st.executeQuery("SELECT datname FROM pg_catalog.pg_database WHERE LOWER(datname) LIKE LOWER('" + sanitizedDbName + "%')"); //NON-NLS + + if (!answer.next()) { + unique = true; + } else { + // not unique. add numbers before dbName. + if (number == Integer.MAX_VALUE) { + // oops. it never became unique. give up. + throw new Exception(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.NonUniqueDatabaseName")); + } + sanitizedDbName = "_" + Integer.toString(number) + "_" + baseDbName; //NON-NLS + + // Chop full db name to 63 characters (max for PostgreSQL) + if (sanitizedDbName.length() > MAX_DB_NAME_LENGTH) { + sanitizedDbName = sanitizedDbName.substring(0, MAX_DB_NAME_LENGTH); + } + ++number; + } + } + dbNameConnection.close(); + } else { + // Could be caused by database credentials, using user accounts that + // can not check if other databases exist, so allow it to continue + log(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.PotentiallyNonUniqueDatabaseName")); + } + + return sanitizedDbName; + } + + /** + * Get the images from the old case and place them in the central + * repository, if the user chose to. + * + * @param icd the Import Case Data + * + * @throws IOException + */ + private void copyImages(ImportCaseData icd) throws IOException { + if (copySourceImages) { + File imageSource = findInputFolder(icd); // Find the folder for the input images + File imageDestination = new File(icd.getSpecificImageOutputFolder().toString()); + + // If we can find the input images, copy if needed. + if (imageSource.exists()) { + FileUtils.copyDirectory(imageSource, imageDestination); + + } else { + log(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.UnableToCopySourceImages")); + } + } + } + + /** + * Fix up any paths in the database that refer to items that have moved. + * Candidates include events.db, input images, reports, file paths, etc. + * + * @param icd the import case data for the current case + * @param dbName the name of the database + */ + private void fixPaths(ImportCaseData icd, String dbName) throws SQLException { + /// Fix paths in reports, tsk_files_path, and tsk_image_names tables + + String input = icd.getSpecificImageInputFolder().toString(); + String output = icd.getSpecificImageOutputFolder().toString(); + + Connection postgresqlConnection = DriverManager.getConnection("jdbc:postgresql://" + db.getHost() + ":" + db.getPort() + "/" + dbName, db.getUserName(), db.getPassword()); //NON-NLS + if (postgresqlConnection != null) { + String hostName = NetworkUtils.getLocalHostName(); + + // add hostname to reports + Statement updateStatement = postgresqlConnection.createStatement(); + updateStatement.executeUpdate("UPDATE reports SET path=CONCAT('" + hostName + "/', path) WHERE path IS NOT NULL AND path != ''"); //NON-NLS + + // add hostname to tsk_files_path + updateStatement = postgresqlConnection.createStatement(); + updateStatement.executeUpdate("UPDATE tsk_files_path SET path=CONCAT('" + hostName + "\\', path) WHERE path IS NOT NULL AND path != ''"); //NON-NLS + + String caseName = TimeStampUtils.removeTimeStamp(icd.getOldCaseName()).toLowerCase(); + + if (copySourceImages) { + // update path for images + Statement inputStatement = postgresqlConnection.createStatement(); + ResultSet inputResultSet = inputStatement.executeQuery("SELECT * FROM tsk_image_names"); //NON-NLS + + while (inputResultSet.next()) { + Path oldPath = Paths.get(inputResultSet.getString(2)); + + for (int x = 0; x < oldPath.getNameCount(); ++x) { + if (oldPath.getName(x).toString().toLowerCase().equals(caseName)) { + Path newPath = Paths.get(output, oldPath.subpath(x + 1, oldPath.getNameCount()).toString()); + updateStatement = postgresqlConnection.createStatement(); + updateStatement.executeUpdate("UPDATE tsk_image_names SET name='" + newPath.toString() + "' WHERE obj_id = " + inputResultSet.getInt(1)); //NON-NLS + break; + } + } + } + } + postgresqlConnection.close(); + } else { + log(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.CanNotOpenDatabase")); + } + } + + /** + * Return an integer from the ResultSet converted to String or NULL, by + * checking ResultSet.wasNull() + * + * @param rs the ResultSet to work with + * @param index the index into the ResultSet to work with + * + * @return the proper value, the integer, or NULL + * + * @throws SQLException + */ + private String getNullableInt(ResultSet rs, int index) throws SQLException { + int value = rs.getInt(index); + if (rs.wasNull()) { + return "NULL"; //NON-NLS + } else { + return Integer.toString(value); + } + } + + /** + * Return a long from the ResultSet converted to String or NULL, by checking + * ResultSet.wasNull() + * + * @param rs the ResultSet to work with + * @param index the index into the ResultSet to work with + * + * @return the proper value, the long, or NULL + * + * @throws SQLException + */ + private String getNullableLong(ResultSet rs, int index) throws SQLException { + long value = rs.getLong(index); + if (rs.wasNull()) { + return "NULL"; //NON-NLS + } else { + return Long.toString(value); + } + } + + /** + * Place a NULL inside a prepared statement if needed, otherwise, place the + * String that was in the ResultSet. + * + * @param pst the prepared statement + * @param rs the ResultSet to work with + * @param rsIndex index for the result set + * @param psIndex index for the prepared statement + * + * @throws SQLException + */ + private void populateNullableString(PreparedStatement pst, ResultSet rs, int rsIndex, int psIndex) throws SQLException { + String nullableString = rs.getString(rsIndex); + if (rs.wasNull()) { + pst.setNull(psIndex, java.sql.Types.NULL); + } else { + pst.setString(psIndex, SleuthkitCase.escapeSingleQuotes(nullableString)); + } + } + + /** + * Place a NULL inside a prepared statement if needed, otherwise, place the + * byte array that was in the ResultSet. + * + * @param pst the prepared statement + * @param rs the ResultSet to work with + * @param rsIndex index for the result set + * @param psIndex index for the prepared statement + * + * @throws SQLException + */ + private void populateNullableByteArray(PreparedStatement pst, ResultSet rs, int rsIndex, int psIndex) throws SQLException { + byte[] nullableBytes = rs.getBytes(rsIndex); + if (rs.wasNull()) { + pst.setNull(psIndex, java.sql.Types.NULL); + } else { + pst.setBytes(psIndex, nullableBytes); + } + } + + /** + * Place a NULL inside a prepared statement if needed, otherwise, place the + * double that was in the ResultSet. + * + * @param pst the prepared statement + * @param rs the ResultSet to work with + * @param rsIndex index for the result set + * @param psIndex index for the prepared statement + * + * @throws SQLException + */ + private void populateNullableNumeric(PreparedStatement pst, ResultSet rs, int rsIndex, int psIndex) throws SQLException { + double nullableNumeric = rs.getDouble(rsIndex); + if (rs.wasNull()) { + pst.setNull(psIndex, java.sql.Types.NULL); + } else { + pst.setDouble(psIndex, nullableNumeric); + } + } + + private class ImportCaseData { + + private Path specificCaseInputFolder; + private Path specificCaseOutputFolder; + private Path specificImageInputFolder; + private Path specificImageOutputFolder; + private String relativeCaseName; + private String newCaseName; + private String oldCaseName; + + public Path getSpecificCaseInputFolder() { + return specificCaseInputFolder; + } + + public Path getSpecificCaseOutputFolder() { + return specificCaseOutputFolder; + } + + public Path getSpecificImageInputFolder() { + return specificImageInputFolder; + } + + public Path getSpecificImageOutputFolder() { + return specificImageOutputFolder; + } + + public String getRelativeCaseName() { + return relativeCaseName; + } + + public String getOldCaseName() { + return oldCaseName; + } + + public String getNewCaseName() { + return newCaseName; + } + + public void setSpecificCaseInputFolder(Path caseInputFolder) { + this.specificCaseInputFolder = caseInputFolder; + } + + public void setSpecificCaseOutputFolder(Path caseOutputFolder) { + this.specificCaseOutputFolder = caseOutputFolder; + } + + public void setSpecificImageInputFolder(Path imageInputFolder) { + this.specificImageInputFolder = imageInputFolder; + } + + public void setSpecificImageOutputFolder(Path imageOutputFolder) { + this.specificImageOutputFolder = imageOutputFolder; + } + + public void setRelativeCaseName(Path input, Path aut) { + this.relativeCaseName = input.relativize(aut).toString(); + } + + public void setOldCaseName(String oldCaseName) { + this.oldCaseName = oldCaseName; + } + + public void setNewCaseName(String newCaseName) { + this.newCaseName = newCaseName; + } + + public ImportCaseData(Path p) { + this.specificCaseInputFolder = p; + this.oldCaseName = p.getFileName().toString(); + this.specificCaseOutputFolder = null; + this.specificImageInputFolder = null; + this.specificImageOutputFolder = null; + this.relativeCaseName = null; + this.newCaseName = null; + } + } + + /** + * This is the runnable's run method. It causes the iteration on all .aut + * files in the path, calling processCase for each one. + */ + @Override + public void run() { + openLog(); + boolean result = true; + + // iterate for .aut files + FindDotAutFolders dotAutFolders = new FindDotAutFolders(); + try { + Path walked = Files.walkFileTree(caseInputFolder, dotAutFolders); + } catch (IOException ex) { + log(ex.getMessage()); + result = false; + } + + ArrayList ableToProcess = new ArrayList<>(); + ArrayList unableToProcess = new ArrayList<>(); + + // validate we can convert this .aut file, one by one + for (Path p : dotAutFolders.getTheList()) { + ImportCaseData icd = new ImportCaseData(p); + icd.setRelativeCaseName(caseInputFolder, p); + if (canProcess(icd)) { + ableToProcess.add(icd); + } else { + unableToProcess.add(icd); + } + } + + StringBuilder casesThatWillBeProcessed = new StringBuilder(); + StringBuilder casesThatWillNotBeProcessed = new StringBuilder(); + + casesThatWillBeProcessed.append(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.WillImport")).append(SEP); // NON-NLS + if (ableToProcess.isEmpty()) { + casesThatWillBeProcessed.append(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.None")).append(SEP); // NON-NLS + } else { + for (ImportCaseData icd : ableToProcess) { + casesThatWillBeProcessed.append(icd.getSpecificCaseInputFolder().toString()).append(SEP); + } + } + + if (!unableToProcess.isEmpty()) { + casesThatWillNotBeProcessed.append(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.WillNotImport")).append(SEP); // NON-NLS + for (ImportCaseData icd : unableToProcess) { + casesThatWillNotBeProcessed.append(icd.getSpecificCaseInputFolder().toString()).append(SEP); + } + } + + JTextArea jta = new JTextArea(casesThatWillBeProcessed.toString() + SEP + casesThatWillNotBeProcessed.toString()); + jta.setEditable(false); + JScrollPane jsp = new JScrollPane(jta) { + private static final long serialVersionUID = 1L; + + @Override + public Dimension getPreferredSize() { + return new Dimension(700, 480); + } + }; + + SwingUtilities.invokeLater(() -> { + userAnswer = JOptionPane.showConfirmDialog(WindowManager.getDefault().getMainWindow(), + jsp, + NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.ContinueWithImport"), // NON-NLS + OK_CANCEL_OPTION); + synchronized (threadWaitNotifyLock) { + threadWaitNotifyLock.notify(); + } + }); + + synchronized (threadWaitNotifyLock) { + try { + threadWaitNotifyLock.wait(); + } catch (InterruptedException ex) { + log("Unable to wait for user input"); + } + } + + if (userAnswer == JOptionPane.OK_OPTION) { + // feed .aut files in one by one for processing + for (ImportCaseData icd : ableToProcess) { + if (false == processCase(icd)) { + result = false; + } + } + closeLog(result); + if (notifyOnComplete != null) { + notifyOnComplete.importDoneCallback(result, ""); // NON-NLS + } + } else { + closeLog(result); + if (notifyOnComplete != null) { + notifyOnComplete.importDoneCallback(false, NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.Cancelled")); // NON-NLS + } + } + } + + /** + * Open the case import log in the base output folder. + * + */ + private void openLog() { + File temp = new File(caseOutputFolder); + temp.mkdirs(); + File logFile = Paths.get(caseOutputFolder, CASE_IMPORT_LOG_FILE).toFile(); + try { + writer = new PrintWriter(new BufferedWriter(new FileWriter(logFile, logFile.exists())), true); + } catch (IOException ex) { + writer = null; + Logger.getLogger(SingleUserCaseImporter.class.getName()).log(Level.WARNING, "Error opening log file " + logFile.toString(), ex); + } + log("Starting batch processing of " + caseInputFolder.toString() + " to " + caseOutputFolder); + } + + /** + * Log a message to the case import log in the base output folder. + * + * @param message the message to log. + */ + private void log(String message) { + if (writer != null) { + writer.println(String.format("%s %s", simpleDateFormat.format((Date.from(Instant.now()).getTime())), message)); //NON-NLS + } + } + + /** + * + * Close the case import log in the base output folder. + * + * @param result this informs the log if the end result was successful or + * not. True if all was successful, false otherwise. + */ + private void closeLog(boolean result) { + log("Completed batch processing of " + caseInputFolder.toString() + " to " + caseOutputFolder + ". Batch processing result: " + ((result == true) ? "Success" : "Failure")); + if (writer != null) { + writer.close(); + } + } + + /** + * This class extends SimpleFileVisitor to find all the cases to process + * based upon .aut files. + */ + private class FindDotAutFolders extends SimpleFileVisitor { + + private final ArrayList theList; + + public FindDotAutFolders() { + this.theList = new ArrayList<>(); + } + + /** + * Handle comparing .aut file and containing folder names without + * timestamps on either one. It strips them off if they exist. + * + * @param directory the directory we are currently visiting. + * @param attrs file attributes. + * + * @return Continue if we want to carry one, SKIP_SUBTREE if we've found + * a .aut file, precluding searching any deeper into this + * folder. + * + * @throws IOException + */ + @Override + public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attrs) throws IOException { + // find all files that end in .aut + File[] dotAutFiles = directory.toFile().listFiles((File dir, String name) -> name.toLowerCase().endsWith(DOTAUT)); + + for (File specificFile : dotAutFiles) { + // if it ends in a timestamp, strip it off + String sanitizedCaseName = specificFile.getName(); + if (TimeStampUtils.endsWithTimeStamp(sanitizedCaseName)) { + sanitizedCaseName = sanitizedCaseName.substring(0, sanitizedCaseName.length() - TimeStampUtils.getTimeStampLength()); + } + + // if folder ends in a timestamp, strip it off + String sanitizedFolderName = directory.getFileName().toString(); + if (TimeStampUtils.endsWithTimeStamp(sanitizedFolderName)) { + sanitizedFolderName = sanitizedFolderName.substring(0, sanitizedFolderName.length() - TimeStampUtils.getTimeStampLength()); + } + + // If file and folder match, found leaf node case + if (sanitizedCaseName.toLowerCase().startsWith(sanitizedFolderName.toLowerCase())) { + theList.add(directory); + return FileVisitResult.SKIP_SUBTREE; + } + } + + // If no matching .aut files, traverse subfolders + return FileVisitResult.CONTINUE; + } + + /** + * This returns the list of folders we've found that need to be looked + * at for possible import as multi-user cases. + * + * @return the theList + */ + public ArrayList getTheList() { + return theList; + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/StartupWindow.java b/Core/src/org/sleuthkit/autopsy/casemodule/StartupWindow.java index 6fff0a1ff4..d609c9359f 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/StartupWindow.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/StartupWindow.java @@ -59,6 +59,7 @@ public final class StartupWindow extends JDialog implements StartupWindowInterfa // set the location of the popUp Window on the center of the screen setLocation((screenDimension.width - w) / 2, (screenDimension.height - h) / 2); + setLocationRelativeTo(WindowManager.getDefault().getMainWindow()); welcomeWindow = new CueBannerPanel(); @@ -80,6 +81,7 @@ public final class StartupWindow extends JDialog implements StartupWindowInterfa @Override public void open() { welcomeWindow.refresh(); + setLocationRelativeTo(WindowManager.getDefault().getMainWindow()); setVisible(true); } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/UpdateRecentCases.java b/Core/src/org/sleuthkit/autopsy/casemodule/UpdateRecentCases.java index 147337965b..4d00a60606 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/UpdateRecentCases.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/UpdateRecentCases.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011 Basis Technology Corp. + * Copyright 2011-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -33,8 +33,10 @@ class UpdateRecentCases extends JMenuItem implements DynamicMenuContent { int length; static boolean hasRecentCase = false; - /** the constructor */ - UpdateRecentCases(){ + /** + * the constructor + */ + UpdateRecentCases() { // display last 5 cases. length = RecentCases.LENGTH - 1; } @@ -60,7 +62,7 @@ class UpdateRecentCases extends JMenuItem implements DynamicMenuContent { menuItem.setActionCommand(caseName[i].toUpperCase()); menuItem.addActionListener(new RecentItems(caseName[i], casePath[i])); comps[i] = menuItem; - hasRecentCase = hasRecentCase || true; + hasRecentCase = true; } } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/XMLCaseManagement.java b/Core/src/org/sleuthkit/autopsy/casemodule/XMLCaseManagement.java index c41269f449..d1e6b87a3a 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/XMLCaseManagement.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/XMLCaseManagement.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2012 Basis Technology Corp. + * Copyright 2012-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -29,10 +29,10 @@ import javax.xml.parsers.*; import javax.xml.transform.*; import javax.xml.transform.dom.*; import javax.xml.transform.stream.*; -import org.openide.util.Exceptions; import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.Case.CaseType; +import org.sleuthkit.autopsy.core.RuntimeProperties; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.coreutils.XMLUtil; import org.w3c.dom.*; import org.xml.sax.SAXException; @@ -60,6 +60,7 @@ class XMLCaseManagement implements CaseConfigFileInterface { final static String SCHEMA_VERSION_NAME = "SchemaVersion"; //NON-NLS final static String AUTOPSY_CRVERSION_NAME = "AutopsyCreatedVersion"; //NON-NLS final static String AUTOPSY_MVERSION_NAME = "AutopsySavedVersion"; //NON-NLS + final static String CASE_TEXT_INDEX_NAME = "TextIndexName"; //NON-NLS // folders inside case directory final static String LOG_FOLDER_NAME = "LogFolder"; //NON-NLS final static String LOG_FOLDER_RELPATH = "Log"; //NON-NLS @@ -69,6 +70,8 @@ class XMLCaseManagement implements CaseConfigFileInterface { final static String EXPORT_FOLDER_RELPATH = "Export"; //NON-NLS final static String CACHE_FOLDER_NAME = "CacheFolder"; //NON-NLS final static String CACHE_FOLDER_RELPATH = "Cache"; //NON-NLS + final static String CASE_TYPE = "CaseType"; //NON-NLS + final static String DATABASE_NAME = "DatabaseName"; //NON-NLS // folders attribute final static String RELATIVE_NAME = "Relative"; // relevant path info NON-NLS // folder attr values @@ -77,23 +80,26 @@ class XMLCaseManagement implements CaseConfigFileInterface { // the document private Document doc; // general info - private DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss (z)"); + private final DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss (z)"); private String caseDirPath; // case directory path private String caseName; // case name - private String caseNumber; // case number + private String caseNumber; // case number private String examiner; // examiner name - private String schemaVersion = "1.0"; - private String autopsySavedVersion; + private final String schemaVersion = "1.0"; + private final String autopsySavedVersion; + private CaseType caseType; // The type of case: local or shared + private String dbName; // The name of the database + private String textIndexName; // The name of the index where extracted text is stored. + // for error handling private JPanel caller; - private String className = this.getClass().toString(); + private final String className = this.getClass().toString(); private static final Logger logger = Logger.getLogger(XMLCaseManagement.class.getName()); /** * The constructor */ - XMLCaseManagement() { -// System.setProperty("netbeans.buildnumber", autopsyVer); // set the current autopsy version // moved to CoreComponents installer + public XMLCaseManagement() { autopsySavedVersion = System.getProperty("netbeans.buildnumber"); } @@ -148,6 +154,23 @@ class XMLCaseManagement implements CaseConfigFileInterface { } + /** + * Sets the created date on the XML configuration file. This method is for + * preserving the created date when converting a case from single-user to + * multi-user. + * + * @param createdDate the date the case was originally created + * + * @throws org.sleuthkit.autopsy.casemodule.CaseActionException + */ + public void setCreatedDate(String createdDate) throws CaseActionException { + String newDate = dateFormat.format(new Date()); + Element rootEl = getRootElement(); + rootEl.getElementsByTagName(CREATED_DATE_NAME).item(0).setTextContent(createdDate); + rootEl.getElementsByTagName(MODIFIED_DATE_NAME).item(0).setTextContent(newDate); + writeFile(); + } + /** * Sets the examiner on the XML configuration file * @@ -187,6 +210,89 @@ class XMLCaseManagement implements CaseConfigFileInterface { caseNumber = givenCaseNumber; // change this to change the xml file if needed } + /** + * Sets the case type internally (on local variable in this class) + * + * @param givenCaseType the new case type + */ + private void setCaseType(CaseType givenCaseType) { + caseType = givenCaseType; // change this to change the xml file if needed + } + + /** + * Gets the case Type from the document handler. Defaults to local if it + * can't figure it out. + * + * @return caseType from the document handler + */ + public CaseType getCaseType() { + if (doc == null) { + return CaseType.SINGLE_USER_CASE; + } else { + if (getCaseElement().getElementsByTagName(CASE_TYPE).getLength() > 0) { + Element nameElement = (Element) getCaseElement().getElementsByTagName(CASE_TYPE).item(0); + return CaseType.fromString(nameElement.getTextContent()); + } else { + return CaseType.SINGLE_USER_CASE; + } + } + } + + /** + * Sets the database name internally (on local variable in this class) + * + * @param givenDbName the new db name + */ + private void setDatabaseName(String givenDbName) { + dbName = givenDbName; // change this to change the xml file if needed + } + + /** + * Gets the database name from the document handler + * + * @return the database name + */ + public String getDatabaseName() { + if (doc == null) { + return ""; + } else { + if (getCaseElement().getElementsByTagName(DATABASE_NAME).getLength() > 0) { + Element nameElement = (Element) getCaseElement().getElementsByTagName(DATABASE_NAME).item(0); + return nameElement.getTextContent(); + } else { + return ""; /// couldn't find one, so return a blank name + } + } + } + + /** + * Sets the text index name internally (on local variable in this class) + * + * @param textIndexName the new name for the index where extracted text is + * stored for the case. + */ + private void setTextIndexName(String textIndexName) { + this.textIndexName = textIndexName; // change this to change the xml file if needed + } + + /** + * Gets the name of the index where extracted text is stored. + * + * @return the index name + */ + public String getTextIndexName() { + if (doc == null) { + return ""; + } else { + if (getCaseElement().getElementsByTagName(CASE_TEXT_INDEX_NAME).getLength() > 0) { + Element nameElement = (Element) getCaseElement().getElementsByTagName(CASE_TEXT_INDEX_NAME).item(0); + return nameElement.getTextContent(); + } else { + return ""; /// couldn't find one, so return a blank index name + } + } + } + /** * Sets the examiner name internally (on local variable in this class) * @@ -282,7 +388,7 @@ class XMLCaseManagement implements CaseConfigFileInterface { * * @return createdDate the creation date of this case */ - protected String getCreatedDate() { + public String getCreatedDate() { if (doc != null) { Element crDateElement = (Element) getRootElement().getElementsByTagName(CREATED_DATE_NAME).item(0); return crDateElement.getTextContent(); @@ -437,13 +543,17 @@ class XMLCaseManagement implements CaseConfigFileInterface { * Initialize the basic values for a new case management file. Note: this is * the schema version 1.0 * - * @param dirPath case directory path - * @param caseName the name of the config file to be located in the case - * directory - * @param examiner examiner for the case (optional, can be empty string - * @param caseNumber case number (optional), can be empty + * @param dirPath case directory path + * @param caseName the name of the config file to be located in the + * case directory + * @param examiner examiner for the case (optional, can be empty string + * @param caseNumber case number (optional), can be empty + * @param dbName the name of the database. Could be a local path, + * could be a Postgre db name. + * @param textIndexName The name of the index where extracted text is + * stored. */ - protected void create(String dirPath, String caseName, String examiner, String caseNumber) throws CaseActionException { + public void create(String dirPath, String caseName, String examiner, String caseNumber, CaseType caseType, String dbName, String textIndexName) throws CaseActionException { clear(); // clear the previous data // set the case Name and Directory and the parent directory @@ -451,6 +561,9 @@ class XMLCaseManagement implements CaseConfigFileInterface { setName(caseName); setExaminer(examiner); setNumber(caseNumber); + setCaseType(caseType); + setDatabaseName(dbName); + setTextIndexName(textIndexName); DocumentBuilder docBuilder; DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); @@ -522,6 +635,18 @@ class XMLCaseManagement implements CaseConfigFileInterface { cacheElement.setAttribute(RELATIVE_NAME, "true"); //NON-NLS caseElement.appendChild(cacheElement); + Element typeElement = doc.createElement(CASE_TYPE); // ... + typeElement.appendChild(doc.createTextNode(caseType.toString())); + caseElement.appendChild(typeElement); + + Element dbNameElement = doc.createElement(DATABASE_NAME); // ... + dbNameElement.appendChild(doc.createTextNode(dbName)); + caseElement.appendChild(dbNameElement); + + Element indexNameElement = doc.createElement(CASE_TEXT_INDEX_NAME); // ... + indexNameElement.appendChild(doc.createTextNode(textIndexName)); + caseElement.appendChild(indexNameElement); + // write more code if needed ... } @@ -597,19 +722,11 @@ class XMLCaseManagement implements CaseConfigFileInterface { File file = new File(conFilePath); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); - DocumentBuilder db = null; + DocumentBuilder db; try { db = dbf.newDocumentBuilder(); doc = db.parse(file); - } catch (ParserConfigurationException ex) { - throw new CaseActionException( - NbBundle.getMessage(this.getClass(), "XMLCaseManagement.open.exception.errReadXMLFile.msg", - conFilePath), ex); - } catch (SAXException ex) { - throw new CaseActionException( - NbBundle.getMessage(this.getClass(), "XMLCaseManagement.open.exception.errReadXMLFile.msg", - conFilePath), ex); - } catch (IOException ex) { + } catch (ParserConfigurationException | SAXException | IOException ex) { throw new CaseActionException( NbBundle.getMessage(this.getClass(), "XMLCaseManagement.open.exception.errReadXMLFile.msg", conFilePath), ex); @@ -618,10 +735,10 @@ class XMLCaseManagement implements CaseConfigFileInterface { doc.getDocumentElement().normalize(); doc.getDocumentElement().normalize(); - if (!XMLUtil.xmlIsValid(doc, XMLCaseManagement.class, XSDFILE)) { - logger.log(Level.WARNING, "Could not validate against [" + XSDFILE + "], results may not accurate"); //NON-NLS - } - + // TODO: Restore later +// if (!XMLUtil.xmlIsValid(doc, XMLCaseManagement.class, XSDFILE)) { +// logger.log(Level.WARNING, "Could not validate against [" + XSDFILE + "], results may not accurate"); //NON-NLS +// } Element rootEl = doc.getDocumentElement(); String rootName = rootEl.getNodeName(); @@ -629,13 +746,16 @@ class XMLCaseManagement implements CaseConfigFileInterface { if (!rootName.equals(TOP_ROOT_NAME)) { // throw an error ... clear(); - JOptionPane.showMessageDialog(caller, - NbBundle.getMessage(this.getClass(), - "XMLCaseManagement.open.msgDlg.notAutCase.msg", - file.getName(), className), - NbBundle.getMessage(this.getClass(), - "XMLCaseManagement.open.msgDlg.notAutCase.title"), - JOptionPane.ERROR_MESSAGE); + if (RuntimeProperties.coreComponentsAreActive()) { + + JOptionPane.showMessageDialog(caller, + NbBundle.getMessage(this.getClass(), + "XMLCaseManagement.open.msgDlg.notAutCase.msg", + file.getName(), className), + NbBundle.getMessage(this.getClass(), + "XMLCaseManagement.open.msgDlg.notAutCase.title"), + JOptionPane.ERROR_MESSAGE); + } } else { /* * Autopsy Created Version @@ -687,5 +807,8 @@ class XMLCaseManagement implements CaseConfigFileInterface { caseName = ""; caseNumber = ""; examiner = ""; + caseType = CaseType.SINGLE_USER_CASE; + dbName = ""; + textIndexName = ""; } } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/events/AddingDataSourceEvent.java b/Core/src/org/sleuthkit/autopsy/casemodule/events/AddingDataSourceEvent.java new file mode 100644 index 0000000000..0df9c8151c --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/events/AddingDataSourceEvent.java @@ -0,0 +1,61 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.events; + +import java.io.Serializable; +import java.util.UUID; +import javax.annotation.concurrent.Immutable; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.events.AutopsyEvent; + +/** + * Event published when a data source is being added to a case. + */ +@Immutable +public final class AddingDataSourceEvent extends AutopsyEvent implements Serializable { + + private static final long serialVersionUID = 1L; + private final UUID dataSourceId; + + /** + * Constructs an event published when a data source is being added to a + * case. + * + * @param dataSourceId A unique identifier associated with the data source. + * Used to pair this AddingDataSourceEvent with a + * DataSourceAddedEvent or a + * AddingDataSourceFailedEvent. + */ + public AddingDataSourceEvent(UUID dataSourceId) { + super(Case.Events.ADDING_DATA_SOURCE.toString(), null, null); + this.dataSourceId = dataSourceId; + } + + /** + * Gets the unique id for the data source used to pair this + * AddindDataSourceEvent with a a DataSourceAddedEvent or a + * AddingDataSourceFailedEvent. + * + * @return The unique id. + */ + public UUID getDataSourceId() { + return dataSourceId; + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/events/AddingDataSourceFailedEvent.java b/Core/src/org/sleuthkit/autopsy/casemodule/events/AddingDataSourceFailedEvent.java new file mode 100644 index 0000000000..d67570bd79 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/events/AddingDataSourceFailedEvent.java @@ -0,0 +1,59 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.events; + +import java.io.Serializable; +import java.util.UUID; +import javax.annotation.concurrent.Immutable; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.events.AutopsyEvent; + +/** + * Event published when an attempt to add a data source to a case fails. + */ +@Immutable +public final class AddingDataSourceFailedEvent extends AutopsyEvent implements Serializable { + + private static final long serialVersionUID = 1L; + private final UUID dataSourceId; + + /** + * Constructs an event published when an attempt to add a data source to a + * case fails. + * + * @param dataSourceId A unique identifier associated with the data source. + * Used to pair this AddingDataSourceFailedEvent with a + * AddingDataSourceEvent. + */ + public AddingDataSourceFailedEvent(UUID dataSourceId) { + super(Case.Events.ADDING_DATA_SOURCE_FAILED.toString(), null, null); + this.dataSourceId = dataSourceId; + } + + /** + * Gets the unique id for the data source used to pair this + * AddingDataSourceFailedEvent with a AddingDataSourceEvent. + * + * @return The unique id. + */ + public UUID getDataSourceId() { + return dataSourceId; + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/events/BlackBoardArtifactTagAddedEvent.java b/Core/src/org/sleuthkit/autopsy/casemodule/events/BlackBoardArtifactTagAddedEvent.java similarity index 55% rename from Core/src/org/sleuthkit/autopsy/events/BlackBoardArtifactTagAddedEvent.java rename to Core/src/org/sleuthkit/autopsy/casemodule/events/BlackBoardArtifactTagAddedEvent.java index d50bd2237b..3c3d15aec8 100644 --- a/Core/src/org/sleuthkit/autopsy/events/BlackBoardArtifactTagAddedEvent.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/events/BlackBoardArtifactTagAddedEvent.java @@ -16,18 +16,36 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.events; +package org.sleuthkit.autopsy.casemodule.events; +import java.io.Serializable; +import javax.annotation.concurrent.Immutable; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.datamodel.BlackboardArtifactTag; +import org.sleuthkit.datamodel.TskCoreException; /** - * + * Event sent when a black board artifact tag is added. */ -public class BlackBoardArtifactTagAddedEvent extends TagAddedEvent { +@Immutable +public class BlackBoardArtifactTagAddedEvent extends TagAddedEvent implements Serializable { + + private static final long serialVersionUID = 1L; public BlackBoardArtifactTagAddedEvent(BlackboardArtifactTag newTag) { - super(Case.Events.BLACKBOARD_ARTIFACT_TAG_ADDED.toString(), newTag - ); + super(Case.Events.BLACKBOARD_ARTIFACT_TAG_ADDED.toString(), newTag); + } + + /** + * get the BlackboardArtifactTag that was added by its id + * + * @return BlackboardArtifactTag that was added + * + * @throws IllegalStateException + * @throws TskCoreException + */ + @Override + BlackboardArtifactTag getTagByID() throws IllegalStateException, TskCoreException { + return Case.getCurrentCase().getServices().getTagsManager().getBlackboardArtifactTagByTagID(getTagID()); } } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/events/BlackBoardArtifactTagDeletedEvent.java b/Core/src/org/sleuthkit/autopsy/casemodule/events/BlackBoardArtifactTagDeletedEvent.java new file mode 100644 index 0000000000..9afef66f37 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/events/BlackBoardArtifactTagDeletedEvent.java @@ -0,0 +1,76 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.events; + +import java.io.Serializable; +import javax.annotation.concurrent.Immutable; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.datamodel.BlackboardArtifactTag; + +/** + * Event that is fired when a black board artifact tag is deleted. + */ +@Immutable +public class BlackBoardArtifactTagDeletedEvent extends TagDeletedEvent implements Serializable { + + private static final long serialVersionUID = 1L; + + public BlackBoardArtifactTagDeletedEvent(BlackboardArtifactTag deletedTag) { + super(Case.Events.BLACKBOARD_ARTIFACT_TAG_DELETED.toString(), new DeletedBlackboardArtifactTagInfo(deletedTag)); + } + + /** + * {@inheritDoc } + * + * @return the DeletedBlackboardArtifactTagInfo for the deleted tag + */ + @Override + public DeletedBlackboardArtifactTagInfo getDeletedTagInfo() { + return (DeletedBlackboardArtifactTagInfo) getOldValue(); + } + + /** + * Extension of {@link DeletedTagInfo} for BlackBoardArtifactTags that + * includes artifact related info. + */ + @Immutable + public static class DeletedBlackboardArtifactTagInfo extends DeletedTagInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + private final long contentID; + + private final long artifactID; + + private DeletedBlackboardArtifactTagInfo(BlackboardArtifactTag deletedTag) { + super(deletedTag); + artifactID = deletedTag.getArtifact().getArtifactID(); + contentID = deletedTag.getContent().getId(); + } + + @Override + public long getContentID() { + return contentID; + } + + public long getArtifactID() { + return artifactID; + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/events/ContentTagAddedEvent.java b/Core/src/org/sleuthkit/autopsy/casemodule/events/ContentTagAddedEvent.java similarity index 64% rename from Core/src/org/sleuthkit/autopsy/events/ContentTagAddedEvent.java rename to Core/src/org/sleuthkit/autopsy/casemodule/events/ContentTagAddedEvent.java index ffd196a4c7..966138434a 100644 --- a/Core/src/org/sleuthkit/autopsy/events/ContentTagAddedEvent.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/events/ContentTagAddedEvent.java @@ -16,20 +16,35 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.events; +package org.sleuthkit.autopsy.casemodule.events; +import java.io.Serializable; import javax.annotation.concurrent.Immutable; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.datamodel.ContentTag; +import org.sleuthkit.datamodel.TskCoreException; /** * An event that is fired when a ContentTag is added. */ @Immutable -public class ContentTagAddedEvent extends TagAddedEvent { +public class ContentTagAddedEvent extends TagAddedEvent implements Serializable { + + private static final long serialVersionUID = 1L; public ContentTagAddedEvent(ContentTag newTag) { super(Case.Events.CONTENT_TAG_ADDED.toString(), newTag); } + /** + * get the ContentTag that was added by its id + * + * @return ContentTag that was added + * + * @throws IllegalStateException + * @throws TskCoreException + */ + ContentTag getTagByID() throws IllegalStateException, TskCoreException { + return Case.getCurrentCase().getServices().getTagsManager().getContentTagByTagID(getTagID()); + } } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/events/ContentTagDeletedEvent.java b/Core/src/org/sleuthkit/autopsy/casemodule/events/ContentTagDeletedEvent.java new file mode 100644 index 0000000000..406ef58a0b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/events/ContentTagDeletedEvent.java @@ -0,0 +1,82 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.events; + +import java.io.Serializable; +import javax.annotation.concurrent.Immutable; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.datamodel.ContentTag; + +/** + * An event that is fired when a ContentTag is deleted. + */ +@Immutable +public class ContentTagDeletedEvent extends TagDeletedEvent implements Serializable { + + private static final long serialVersionUID = 1L; + + public ContentTagDeletedEvent(ContentTag deletedTag) { + super(Case.Events.CONTENT_TAG_DELETED.toString(), new DeletedContentTagInfo(deletedTag)); + } + + /** + * {@inheritDoc } + * + * @return the DeletedContentTagInfo for the deleted tag + */ + @Override + public DeletedContentTagInfo getDeletedTagInfo() { + return (DeletedContentTagInfo) getOldValue(); + } + + /** + * Extension of {@link DeletedTagInfo} for BlackBoardArtifactTags that + * includes byte offset related info. + */ + @Immutable + public static class DeletedContentTagInfo extends DeletedTagInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + private final long contentID; + private final long beginByteOffset; + private final long endByteOffset; + + private DeletedContentTagInfo(ContentTag deletedTag) { + super(deletedTag); + beginByteOffset = deletedTag.getBeginByteOffset(); + endByteOffset = deletedTag.getEndByteOffset(); + contentID = deletedTag.getContent().getId(); + + } + + @Override + public long getContentID() { + return contentID; + } + + public long getBeginByteOffset() { + return beginByteOffset; + } + + public long getEndByteOffset() { + return endByteOffset; + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/events/DataSourceAddedEvent.java b/Core/src/org/sleuthkit/autopsy/casemodule/events/DataSourceAddedEvent.java new file mode 100644 index 0000000000..bba2fba8c3 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/events/DataSourceAddedEvent.java @@ -0,0 +1,108 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.events; + +import java.io.Serializable; +import java.util.UUID; +import java.util.logging.Level; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.events.AutopsyEvent; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Event published when a data source is added to a case. + */ +public final class DataSourceAddedEvent extends AutopsyEvent implements Serializable { + + private static final long serialVersionUID = 1L; + private static final Logger logger = Logger.getLogger(DataSourceAddedEvent.class.getName()); + private transient Content dataSource; + private final UUID dataSourceId; + + /** + * Constructs an event published when a data source is added to a case. + * + * @param dataSource The data source that was added. + * @param dataSourceId A unique identifier associated with the data source. + * Used to pair this DataSourceAddedEvent with a + * AddindDataSourceEvent. + */ + public DataSourceAddedEvent(Content dataSource, UUID dataSourceId) { + /** + * Putting the object id of the data source into newValue to allow for + * lazy loading of the Content object. This bypasses the issues related + * to the serialization and de-serialization of Content objects when the + * event is published over a network. + */ + super(Case.Events.DATA_SOURCE_ADDED.toString(), null, dataSource.getId()); + this.dataSource = dataSource; + this.dataSourceId = dataSourceId; + } + + /** + * Gets the data source that was added. + * + * @return The data source or null if there is an error retrieving the data + * source. + */ + @Override + public Object getNewValue() { + /** + * The dataSource field is set in the constructor, but it is transient + * so it will become null when the event is serialized for publication + * over a network. Doing a lazy load of the Content object bypasses the + * issues related to the serialization and de-serialization of Content + * objects and may also save database round trips from other nodes since + * subscribers to this event are often not interested in the event data. + */ + if (null != dataSource) { + return dataSource; + } + try { + long id = (Long) super.getNewValue(); + dataSource = Case.getCurrentCase().getSleuthkitCase().getContentById(id); + return dataSource; + } catch (IllegalStateException | TskCoreException ex) { + logger.log(Level.SEVERE, "Error doing lazy load for remote event", ex); + return null; + } + } + + /** + * Gets the data source that was added. + * + * @return The data source. + */ + public Content getDataSource() { + return (Content) getNewValue(); + } + + /** + * Gets the unique id for the data source used to pair this + * DataSourceAddedEvent with a AddingDataSourceEvent. + * + * @return The unique id. + */ + public UUID getDataSourceId() { + return dataSourceId; + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/events/ReportAddedEvent.java b/Core/src/org/sleuthkit/autopsy/casemodule/events/ReportAddedEvent.java new file mode 100644 index 0000000000..84121ad2de --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/events/ReportAddedEvent.java @@ -0,0 +1,86 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.events; + +import java.io.Serializable; +import java.util.List; +import java.util.logging.Level; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.events.AutopsyEvent; +import org.sleuthkit.datamodel.Report; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Event published when a report is added to a case. + */ +public final class ReportAddedEvent extends AutopsyEvent implements Serializable { + + private static final long serialVersionUID = 1L; + private static final Logger logger = Logger.getLogger(DataSourceAddedEvent.class.getName()); + private transient Report report; + + /** + * Constructs an event published when a report is added to a case. + * + * @param report The data source that was added. + */ + public ReportAddedEvent(Report report) { + /** + * Putting the object id of the report into newValue to allow for lazy + * loading of the Report object. + */ + super(Case.Events.REPORT_ADDED.toString(), null, report.getId()); + this.report = report; + } + + /** + * Gets the data source that was added. + * + * @return The data source. + */ + @Override + public Object getNewValue() { + /** + * The report field is set in the constructor, but it is transient so it + * will become null when the event is serialized for publication over a + * network. Doing a lazy load of the Report object may save database + * round trips from other nodes since subscribers to this event are + * often not interested in the event data. + */ + if (null != report) { + return report; + } + try { + long id = (Long) super.getNewValue(); + List reports = Case.getCurrentCase().getSleuthkitCase().getAllReports(); + for (Report thisReport : reports) { + if (thisReport.getId() == id) { + report = thisReport; + break; + } + } + return report; + } catch (IllegalStateException | TskCoreException ex) { + logger.log(Level.SEVERE, "Error doing lazy load for remote event", ex); + return null; + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/events/TagAddedEvent.java b/Core/src/org/sleuthkit/autopsy/casemodule/events/TagAddedEvent.java new file mode 100644 index 0000000000..88cbd023bb --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/events/TagAddedEvent.java @@ -0,0 +1,105 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.events; + +import java.io.Serializable; +import java.util.logging.Level; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.events.AutopsyEvent; +import org.sleuthkit.datamodel.Tag; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Base Class for events that are fired when a Tag is added + */ +abstract class TagAddedEvent extends AutopsyEvent implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * The tag that was added. This will be lost during serialization and + * re-loaded from the database in getNewValue() + */ + private transient T tag; + + /** + * The id of the tag that was added. This will bu used to re-load the + * transient tag from the database. + */ + private final Long tagID; + + TagAddedEvent(String propertyName, T addedTag) { + super(propertyName, null, null); + tag = addedTag; + tagID = addedTag.getId(); + } + + /** + * get the id of the Tag that was added + * + * @return the id of the Tag that was added + */ + Long getTagID() { + return tagID; + } + + /** + * get the Tag that was added + * + * @return the tTag + */ + public T getAddedTag() { + return getNewValue(); + } + + @Override + public T getNewValue() { + /** + * The tag field is set in the constructor, but it is transient so it + * will become null when the event is serialized for publication over a + * network. Doing a lazy load of the Tag object bypasses the issues + * related to the serialization and de-serialization of Tag objects and + * may also save database round trips from other nodes since subscribers + * to this event are often not interested in the event data. + */ + if (null != tag) { + return tag; + } + try { + tag = getTagByID(); + return tag; + } catch (IllegalStateException | TskCoreException ex) { + Logger.getLogger(TagAddedEvent.class.getName()).log(Level.SEVERE, "Error doing lazy load for remote event", ex); + return null; + } + } + + /** + * implementors should override this to lookup the appropriate kind of tag + * (Content/BlackBoardArtifact) during the lazy load of the transient tag + * field + * + * + * @return the Tag based on the saved tag id + * + * @throws IllegalStateException + * @throws TskCoreException + */ + abstract T getTagByID() throws IllegalStateException, TskCoreException; +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/events/TagDeletedEvent.java b/Core/src/org/sleuthkit/autopsy/casemodule/events/TagDeletedEvent.java new file mode 100644 index 0000000000..a0ba651f53 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/events/TagDeletedEvent.java @@ -0,0 +1,87 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.events; + +import java.io.Serializable; +import javax.annotation.concurrent.Immutable; +import org.sleuthkit.autopsy.events.AutopsyEvent; +import org.sleuthkit.datamodel.Tag; +import org.sleuthkit.datamodel.TagName; + +/** + * Base Class for events that are fired when a Tag is deleted + */ +@Immutable +abstract class TagDeletedEvent extends AutopsyEvent implements Serializable { + + private static final long serialVersionUID = 1L; + + TagDeletedEvent(String propertyName, DeletedTagInfo deletedTagInfo) { + super(propertyName, deletedTagInfo, null); + } + + /** + * get info about the Tag that was deleted. + * + * Since we don't serialize the deleted tag, and we can't look it up by id + * after it is deleted, we send this info to represent it. + * + * @return info about the Tag that was deleted. + */ + @SuppressWarnings("unchecked") + abstract public DeletedTagInfo getDeletedTagInfo(); + + /** + * Base Class for info about a deleted tag. This is sent as the old value in + * the event, since we are not serializing the Tag it self, and we can't + * look it up by id, like we do for added tags, because it doesn't exist in + * the db any more. + * + * @param the subtype of Tag, BlackBoardArtifactTag or ContentTag + */ + @Immutable + abstract static class DeletedTagInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + private final String comment; + private final long tagID; + private final TagName name; + + DeletedTagInfo(T deletedTag) { + comment = deletedTag.getComment(); + tagID = deletedTag.getId(); + name = deletedTag.getName(); + } + + abstract public long getContentID(); + + public String getComment() { + return comment; + } + + public long getTagID() { + return tagID; + } + + public TagName getName() { + return name; + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/services/Bundle.properties b/Core/src/org/sleuthkit/autopsy/casemodule/services/Bundle.properties index b267fde0a2..67ed2fe8cd 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/services/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/casemodule/services/Bundle.properties @@ -15,7 +15,7 @@ TagsManager.addContentTag.exception.beginByteOffsetOOR.msg=beginByteOffset \= {0 TagsManager.addContentTag.exception.endByteOffsetOOR.msg=endByteOffset \= {0} out of content size range (0 - {1}) TagsManager.addContentTag.exception.endLTbegin.msg=endByteOffset < beginByteOffset TagsManager.predefTagNames.bookmark.text=Bookmark -TagsManager.addContentTag.noCaseWarning=Failed to add publish new content tag event. There is no case open. -TagsManager.deleteContentTag.noCaseWarning=Failed to add publish content tag deleted event. There is no case open. -TagsManager.addBlackboardArtifactTag.noCaseWarning=Failed to add publish new blackboard artifact tag event. There is no case open. -TagsManager.deleteBlackboardArtifactTag.noCaseWarning=Failed to add publish blackboard artifact tag deleted event. There is no case open. +TagsManager.addContentTag.noCaseWarning=Failed to publish new content tag event. There is no case open. +TagsManager.deleteContentTag.noCaseWarning=Failed to publish content tag deleted event. There is no case open. +TagsManager.addBlackboardArtifactTag.noCaseWarning=Failed to publish new blackboard artifact tag event. There is no case open. +TagsManager.deleteBlackboardArtifactTag.noCaseWarning=Failed to publish blackboard artifact tag deleted event. There is no case open. diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/services/TagsManager.java b/Core/src/org/sleuthkit/autopsy/casemodule/services/TagsManager.java index aa79469d4e..7501cf052a 100755 --- a/Core/src/org/sleuthkit/autopsy/casemodule/services/TagsManager.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/services/TagsManager.java @@ -272,7 +272,7 @@ public class TagsManager implements Closeable { tskCase.deleteContentTag(tag); try { Case.getCurrentCase().notifyContentTagDeleted(tag); - } catch (IllegalArgumentException e) { + } catch (IllegalStateException e) { Logger.getLogger(TagsManager.class.getName()).log(Level.WARNING, NbBundle.getMessage(TagsManager.class, "TagsManager.deleteContentTag.noCaseWarning")); } } @@ -311,6 +311,24 @@ public class TagsManager implements Closeable { return tskCase.getContentTagsCountByTagName(tagName); } + /** + * Gets a content tag by tag id. + * + * @param tagID The tag id of interest. + * + * @return the content tag with the specified tag id. + * + * @throws TskCoreException + */ + public synchronized ContentTag getContentTagByTagID(long tagID) throws TskCoreException { + // @@@ This is a work around to be removed when database access on the EDT is correctly synchronized. + if (!tagNamesInitialized) { + getExistingTagNames(); + } + + return tskCase.getContentTagByID(tagID); + } + /** * Gets content tags by tag name. * @@ -385,7 +403,7 @@ public class TagsManager implements Closeable { BlackboardArtifactTag addBlackboardArtifactTag = tskCase.addBlackboardArtifactTag(artifact, tagName, comment); try { Case.getCurrentCase().notifyBlackBoardArtifactTagAdded(addBlackboardArtifactTag); - } catch (IllegalArgumentException e) { + } catch (IllegalStateException e) { Logger.getLogger(TagsManager.class.getName()).log(Level.WARNING, NbBundle.getMessage(TagsManager.class, "TagsManager.addBlackboardArtifactTag.noCaseWarning")); } return addBlackboardArtifactTag; @@ -407,7 +425,7 @@ public class TagsManager implements Closeable { tskCase.deleteBlackboardArtifactTag(tag); try { Case.getCurrentCase().notifyBlackBoardArtifactTagDeleted(tag); - } catch (IllegalArgumentException e) { + } catch (IllegalStateException e) { Logger.getLogger(TagsManager.class.getName()).log(Level.WARNING, NbBundle.getMessage(TagsManager.class, "TagsManager.deleteBlackboardArtifactTag.noCaseWarning")); } } @@ -447,6 +465,24 @@ public class TagsManager implements Closeable { return tskCase.getBlackboardArtifactTagsCountByTagName(tagName); } + /** + * Gets a blackboard artifact tag by tag id. + * + * @param tagID The tag id of interest. + * + * @return the blackboard artifact tag with the specified tag id. + * + * @throws TskCoreException + */ + public synchronized BlackboardArtifactTag getBlackboardArtifactTagByTagID(long tagID) throws TskCoreException { + // @@@ This is a work around to be removed when database access on the EDT is correctly synchronized. + if (!tagNamesInitialized) { + getExistingTagNames(); + } + + return tskCase.getBlackboardArtifactTagByID(tagID); + } + /** * Gets blackboard artifact tags by tag name. * diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties index 6fd097e565..38eb6a3599 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties @@ -12,7 +12,7 @@ Metadata.tableRowTitle.hashLookupResults=Hash Lookup Results Metadata.tableRowTitle.internalid=Internal ID Metadata.tableRowTitle.localPath=Local Path Metadata.tableRowTitle.type=Type -Metadata.title=Metadata +Metadata.title=File Metadata Metadata.toolTip=Displays metadata about the file. Metadata.nodeText.nonFilePassedIn=Non-file passed in Metadata.nodeText.text=From The Sleuth Kit istat Tool\: diff --git a/Core/src/org/sleuthkit/autopsy/core/Bundle.properties b/Core/src/org/sleuthkit/autopsy/core/Bundle.properties index ff08a90d89..913f6d8c82 100644 --- a/Core/src/org/sleuthkit/autopsy/core/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/core/Bundle.properties @@ -12,3 +12,15 @@ org_sleuthkit_autopsy_core_update_center=http://sleuthkit.org/autopsy/updates.xm Services/AutoupdateType/org_sleuthkit_autopsy_core_update_center.settings=Autopsy Update Center Installer.errorInitJavafx.msg=Error initializing JavaFX. Installer.errorInitJavafx.details=\ Some features will not be available. Check that you have the right JRE installed (Oracle JRE > 1.7.10). +ServicesMonitor.failedService.notify.title=Service Is Down +ServicesMonitor.failedService.notify.msg=Connection to {0} is down +ServicesMonitor.restoredService.notify.title=Service Is Up +ServicesMonitor.restoredService.notify.msg=Connection to {0} is up +ServicesMonitor.statusChange.notify.title=Service Status Update +ServicesMonitor.statusChange.notify.msg=Status for {0} is {1} +ServicesMonitor.nullServiceName.excepton.txt=Requested service name is null +ServicesMonitor.unknownServiceName.excepton.txt=Requested service name {0} is unknown +TextConverter.convert.exception.txt=Unable to convert text {0} to hex text +TextConverter.convertFromHex.exception.txt=Unable to convert hex text to text +ServicesMonitor.KeywordSearchNull=Cannot find Keyword Search service +ServicesMonitor.InvalidPortNumber=Invalid port number. \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/core/Installer.java b/Core/src/org/sleuthkit/autopsy/core/Installer.java index 8f43e21eef..3c42f3aac4 100644 --- a/Core/src/org/sleuthkit/autopsy/core/Installer.java +++ b/Core/src/org/sleuthkit/autopsy/core/Installer.java @@ -66,9 +66,10 @@ public class Installer extends ModuleInstall { //We should update this if we officially switch to a new version of CRT/compiler System.loadLibrary("msvcr100"); //NON-NLS System.loadLibrary("msvcp100"); //NON-NLS - logger.log(Level.INFO, "MS CRT libraries loaded"); //NON-NLS + System.loadLibrary("msvcr120"); //NON-NLS + logger.log(Level.INFO, "MSVCR100 and MSVCP100 libraries loaded"); //NON-NLS } catch (UnsatisfiedLinkError e) { - logger.log(Level.SEVERE, "Error loading ms crt libraries, ", e); //NON-NLS + logger.log(Level.SEVERE, "Error loading MSVCR100 and MSVCP100 libraries, ", e); //NON-NLS } try { @@ -84,6 +85,34 @@ public class Installer extends ModuleInstall { } catch (UnsatisfiedLinkError e) { logger.log(Level.SEVERE, "Error loading EWF library, ", e); //NON-NLS } + + try { + System.loadLibrary("libeay32"); //NON-NLS + logger.log(Level.INFO, "LIBEAY32 library loaded"); //NON-NLS + } catch (UnsatisfiedLinkError e) { + logger.log(Level.SEVERE, "Error loading LIBEAY32 library, ", e); //NON-NLS + } + + try { + System.loadLibrary("ssleay32"); //NON-NLS + logger.log(Level.INFO, "SSLEAY32 library loaded"); //NON-NLS + } catch (UnsatisfiedLinkError e) { + logger.log(Level.SEVERE, "Error loading SSLEAY32 library, ", e); //NON-NLS + } + + try { + System.loadLibrary("libintl-8"); //NON-NLS + logger.log(Level.INFO, "libintl-8 library loaded"); //NON-NLS + } catch (UnsatisfiedLinkError e) { + logger.log(Level.SEVERE, "Error loading libintl-8 library, ", e); //NON-NLS + } + + try { + System.loadLibrary("libpq"); //NON-NLS + logger.log(Level.INFO, "LIBPQ library loaded"); //NON-NLS + } catch (UnsatisfiedLinkError e) { + logger.log(Level.SEVERE, "Error loading LIBPQ library, ", e); //NON-NLS + } } } diff --git a/Core/src/org/sleuthkit/autopsy/core/RuntimeProperties.java b/Core/src/org/sleuthkit/autopsy/core/RuntimeProperties.java new file mode 100644 index 0000000000..c2c74bfff3 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/core/RuntimeProperties.java @@ -0,0 +1,62 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-2015 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.core; + +/** + * Application properties that are set once at runtime and are not saved between + * invocations of Autopsy. + */ +public class RuntimeProperties { + + private static boolean coreComponentsActive = true; + private static boolean coreComponentsActiveSet = false; + + /** + * Sets or unsets a flag indicating whether or not the core Autopsy UI + * components and user interactions with those components via menus, message + * boxes, NetBeans progress handles, etc., are enabled. + *

+ * This flag exists as a mechanism to allow use of Autopsy as a platform + * with the core Autopsy user interface disabled, until such time as the + * user interface is made separable and optional. + * + * @param coreComponentsActive True or false. + */ + public static void setCoreComponentsActive(boolean coreComponentsActive) { + if (!coreComponentsActiveSet) { + RuntimeProperties.coreComponentsActive = coreComponentsActive; + coreComponentsActiveSet = true; + } + } + + /** + * Gets a flag indicating whether or not the core Autopsy UI components and + * user interactions with those components via menus, message boxes, + * NetBeans progress handles, etc., are enabled. + *

+ * This flag exists as a mechanism to allow use of Autopsy as a platform + * with the core Autopsy user interface disabled, until such time as the + * user interface is made separable and optional. + * + * @return True or false. + */ + public static boolean coreComponentsAreActive() { + return coreComponentsActive; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/core/ServicesMonitor.java b/Core/src/org/sleuthkit/autopsy/core/ServicesMonitor.java new file mode 100644 index 0000000000..4c1fb92989 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/core/ServicesMonitor.java @@ -0,0 +1,411 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-2015 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.core; + +import org.sleuthkit.autopsy.core.events.ServiceEvent; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.beans.PropertyChangeListener; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.openide.util.Lookup; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.events.AutopsyEventPublisher; +import org.sleuthkit.autopsy.keywordsearchservice.KeywordSearchService; +import org.sleuthkit.autopsy.events.MessageServiceConnectionInfo; +import org.sleuthkit.autopsy.events.MessageServiceException; +import org.sleuthkit.autopsy.keywordsearchservice.KeywordSearchServiceException; +import org.sleuthkit.datamodel.CaseDbConnectionInfo; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * This class periodically checks availability of collaboration resources - + * remote database, remote keyword search server, messaging service - and + * reports status updates to the user in case of a gap in service. + */ +public class ServicesMonitor { + + private AutopsyEventPublisher eventPublisher; + private static final Logger logger = Logger.getLogger(ServicesMonitor.class.getName()); + private final ScheduledThreadPoolExecutor periodicTasksExecutor; + + private static final String PERIODIC_TASK_THREAD_NAME = "services-monitor-periodic-task-%d"; + private static final int NUMBER_OF_PERIODIC_TASK_THREADS = 1; + private static final long CRASH_DETECTION_INTERVAL_MINUTES = 2; + + private static final Set servicesList = Stream.of(ServicesMonitor.Service.values()) + .map(Service::toString) + .collect(Collectors.toSet()); + + /** + * The service monitor maintains a mapping of each service to it's last + * status update. + */ + private final ConcurrentHashMap statusByService; + + /** + * Call constructor on start-up so that the first check of services is done + * as soon as possible. + */ + private static ServicesMonitor instance = new ServicesMonitor(); + + /** + * List of services that are being monitored. The service names should be + * representative of the service functionality and readable as they get + * logged when service outage occurs. + */ + public enum Service { + + /** + * Property change event fired when remote case database service status + * changes. New value is set to updated ServiceStatus, old value is + * null. + */ + REMOTE_CASE_DATABASE("Multi-user case database service"), + /** + * Property change event fired when remote keyword search service status + * changes. New value is set to updated ServiceStatus, old value is + * null. + */ + REMOTE_KEYWORD_SEARCH("Multi-user keyword search service"), + /** + * Property change event fired when messaging service status changes. + * New value is set to updated ServiceStatus, old value is null. + */ + MESSAGING("Messaging service"); + + private final String displayName; + + private Service(String name) { + this.displayName = name; + } + + public String getDisplayName() { + return displayName; + } + }; + + /** + * List of possible service statuses. + */ + public enum ServiceStatus { + + /** + * Service is currently up. + */ + UP, + /** + * Service is currently down. + */ + DOWN + }; + + public synchronized static ServicesMonitor getInstance() { + if (instance == null) { + instance = new ServicesMonitor(); + } + return instance; + } + + private ServicesMonitor() { + + this.eventPublisher = new AutopsyEventPublisher(); + this.statusByService = new ConcurrentHashMap<>(); + + // First check is triggered immediately on current thread. + checkAllServices(); + + /** + * Start periodic task that check the availability of key collaboration + * services. + */ + periodicTasksExecutor = new ScheduledThreadPoolExecutor(NUMBER_OF_PERIODIC_TASK_THREADS, new ThreadFactoryBuilder().setNameFormat(PERIODIC_TASK_THREAD_NAME).build()); + periodicTasksExecutor.scheduleAtFixedRate(new CrashDetectionTask(), CRASH_DETECTION_INTERVAL_MINUTES, CRASH_DETECTION_INTERVAL_MINUTES, TimeUnit.MINUTES); + } + + /** + * Updates service status and publishes the service status update if it is + * different from previous status. Event is published locally. Logs status + * changes. + * + * @param service Name of the service. + * @param status Updated status for the service. + * @param details Details of the event. + * + */ + public void setServiceStatus(String service, String status, String details) { + // if the status update is for an existing service who's status hasn't changed - do nothing. + if (statusByService.containsKey(service) && status.equals(statusByService.get(service))) { + return; + } + + // new service or status has changed - identify service's display name + String serviceDisplayName; + try { + serviceDisplayName = ServicesMonitor.Service.valueOf(service).getDisplayName(); + } catch (IllegalArgumentException ignore) { + // custom service that is not listed in ServicesMonitor.Service enum. Use service name as display name. + serviceDisplayName = service; + } + + if (status.equals(ServiceStatus.UP.toString())) { + logger.log(Level.INFO, "Connection to {0} is up", serviceDisplayName); //NON-NLS + MessageNotifyUtil.Notify.info(NbBundle.getMessage(ServicesMonitor.class, "ServicesMonitor.restoredService.notify.title"), + NbBundle.getMessage(ServicesMonitor.class, "ServicesMonitor.restoredService.notify.msg", serviceDisplayName)); + } else if (status.equals(ServiceStatus.DOWN.toString())) { + logger.log(Level.SEVERE, "Failed to connect to {0}", serviceDisplayName); //NON-NLS + MessageNotifyUtil.Notify.error(NbBundle.getMessage(ServicesMonitor.class, "ServicesMonitor.failedService.notify.title"), + NbBundle.getMessage(ServicesMonitor.class, "ServicesMonitor.failedService.notify.msg", serviceDisplayName)); + } else { + logger.log(Level.INFO, "Status for {0} is {1}", new Object[]{serviceDisplayName, status}); //NON-NLS + MessageNotifyUtil.Notify.info(NbBundle.getMessage(ServicesMonitor.class, "ServicesMonitor.statusChange.notify.title"), + NbBundle.getMessage(ServicesMonitor.class, "ServicesMonitor.statusChange.notify.msg", new Object[]{serviceDisplayName, status})); + } + + // update and publish new status + statusByService.put(service, status); + eventPublisher.publishLocally(new ServiceEvent(service, status, details)); + } + + /** + * Get last status update for a service. + * + * @param service Name of the service. + * + * @return ServiceStatus Status for the service. + * + * @throws ServicesMonitorException If service name is null or service + * doesn't exist. + */ + public String getServiceStatus(String service) throws ServicesMonitorException { + + if (service == null) { + throw new ServicesMonitorException(NbBundle.getMessage(ServicesMonitor.class, "ServicesMonitor.nullServiceName.excepton.txt")); + } + + // if request is for one of our "core" services - perform an on demand check + // to make sure we have the latest status. + if (servicesList.contains(service)) { + checkServiceStatus(service); + } + + String status = statusByService.get(service); + if (status == null) { + // no such service + throw new ServicesMonitorException(NbBundle.getMessage(ServicesMonitor.class, "ServicesMonitor.unknownServiceName.excepton.txt", service)); + } + return status; + } + + /** + * Performs service availability status check. + * + * @param service Name of the service. + */ + private void checkServiceStatus(String service) { + if (service.equals(Service.REMOTE_CASE_DATABASE.toString())) { + checkDatabaseConnectionStatus(); + } else if (service.equals(Service.REMOTE_KEYWORD_SEARCH.toString())) { + checkKeywordSearchServerConnectionStatus(); + } else if (service.equals(Service.MESSAGING.toString())) { + checkMessagingServerConnectionStatus(); + } + } + + /** + * Performs case database service availability status check. + */ + private void checkDatabaseConnectionStatus() { + CaseDbConnectionInfo info; + try { + info = UserPreferences.getDatabaseConnectionInfo(); + } catch (UserPreferencesException ex) { + logger.log(Level.SEVERE, "Error accessing case database connection info", ex); //NON-NLS + setServiceStatus(Service.REMOTE_CASE_DATABASE.toString(), ServiceStatus.DOWN.toString(), "Error accessing case database connection info"); + return; + } + try { + SleuthkitCase.tryConnect(info); + setServiceStatus(Service.REMOTE_CASE_DATABASE.toString(), ServiceStatus.UP.toString(), ""); + } catch (TskCoreException ex) { + setServiceStatus(Service.REMOTE_CASE_DATABASE.toString(), ServiceStatus.DOWN.toString(), ex.getMessage()); + } + } + + /** + * Performs keyword search service availability status check. + */ + private void checkKeywordSearchServerConnectionStatus() { + KeywordSearchService kwsService = Lookup.getDefault().lookup(KeywordSearchService.class); + try { + if (kwsService != null) { + int port = Integer.parseUnsignedInt(UserPreferences.getIndexingServerPort()); + kwsService.tryConnect(UserPreferences.getIndexingServerHost(), port); + setServiceStatus(Service.REMOTE_KEYWORD_SEARCH.toString(), ServiceStatus.UP.toString(), ""); + } else { + setServiceStatus(Service.REMOTE_KEYWORD_SEARCH.toString(), ServiceStatus.DOWN.toString(), + NbBundle.getMessage(ServicesMonitor.class, "ServicesMonitor.KeywordSearchNull")); + } + } catch (NumberFormatException ex) { + String rootCause = NbBundle.getMessage(ServicesMonitor.class, "ServicesMonitor.InvalidPortNumber"); + logger.log(Level.SEVERE, "Unable to connect to messaging server: " + rootCause, ex); //NON-NLS + setServiceStatus(Service.REMOTE_KEYWORD_SEARCH.toString(), ServiceStatus.DOWN.toString(), rootCause); + } catch (KeywordSearchServiceException ex) { + String rootCause = ex.getMessage(); + logger.log(Level.SEVERE, "Unable to connect to messaging server: " + rootCause, ex); //NON-NLS + setServiceStatus(Service.REMOTE_KEYWORD_SEARCH.toString(), ServiceStatus.DOWN.toString(), rootCause); + } + } + + /** + * Performs messaging service availability status check. + */ + private void checkMessagingServerConnectionStatus() { + MessageServiceConnectionInfo info; + try { + info = UserPreferences.getMessageServiceConnectionInfo(); + } catch (UserPreferencesException ex) { + logger.log(Level.SEVERE, "Error accessing messaging service connection info", ex); //NON-NLS + setServiceStatus(Service.MESSAGING.toString(), ServiceStatus.DOWN.toString(), "Error accessing messaging service connection info"); + return; + } + + try { + info.tryConnect(); + setServiceStatus(Service.MESSAGING.toString(), ServiceStatus.UP.toString(), ""); + } catch (MessageServiceException ex) { + String rootCause = ex.getMessage(); + logger.log(Level.SEVERE, "Unable to connect to messaging server: " + rootCause, ex); //NON-NLS + setServiceStatus(Service.MESSAGING.toString(), ServiceStatus.DOWN.toString(), rootCause); + } + } + + /** + * Adds an event subscriber to this publisher. Subscriber will be subscribed + * to all events from this publisher. + * + * @param subscriber The subscriber to add. + */ + public void addSubscriber(PropertyChangeListener subscriber) { + eventPublisher.addSubscriber(servicesList, subscriber); + } + + /** + * Adds an event subscriber to this publisher. + * + * @param eventNames The events the subscriber is interested in. + * @param subscriber The subscriber to add. + */ + public void addSubscriber(Set eventNames, PropertyChangeListener subscriber) { + eventPublisher.addSubscriber(eventNames, subscriber); + } + + /** + * Adds an event subscriber to this publisher. + * + * @param eventName The event the subscriber is interested in. + * @param subscriber The subscriber to add. + */ + public void addSubscriber(String eventName, PropertyChangeListener subscriber) { + eventPublisher.addSubscriber(eventName, subscriber); + } + + /** + * Removes an event subscriber from this publisher. + * + * @param eventNames The events the subscriber is no longer interested in. + * @param subscriber The subscriber to remove. + */ + public void removeSubscriber(Set eventNames, PropertyChangeListener subscriber) { + eventPublisher.removeSubscriber(eventNames, subscriber); + } + + /** + * Removes an event subscriber from this publisher. + * + * @param eventName The event the subscriber is no longer interested in. + * @param subscriber The subscriber to remove. + */ + public void removeSubscriber(String eventName, PropertyChangeListener subscriber) { + eventPublisher.removeSubscriber(eventName, subscriber); + } + + /** + * Removes an event subscriber to this publisher. Subscriber will be removed + * from all event notifications from this publisher. + * + * @param subscriber The subscriber to remove. + */ + public void removeSubscriber(PropertyChangeListener subscriber) { + eventPublisher.removeSubscriber(servicesList, subscriber); + } + + /** + * Verifies connectivity to all services. + */ + private void checkAllServices() { + if (!UserPreferences.getIsMultiUserModeEnabled()) { + return; + } + + for (String service : servicesList) { + checkServiceStatus(service); + } + } + + /** + * A Runnable task that periodically checks the availability of + * collaboration resources (remote database, remote keyword search service, + * message broker) and reports status to the user in case of a gap in + * service. + */ + private final class CrashDetectionTask implements Runnable { + + /** + * Monitor the availability of collaboration resources + */ + @Override + public void run() { + checkAllServices(); + } + } + + /** + * Exception thrown when service status query results in an error. + */ + public class ServicesMonitorException extends Exception { + + private static final long serialVersionUID = 1L; + + public ServicesMonitorException(String message) { + super(message); + } + + public ServicesMonitorException(String message, Throwable cause) { + super(message, cause); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java b/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java index 5349915871..a9909593b0 100755 --- a/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java +++ b/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java @@ -18,9 +18,20 @@ */ package org.sleuthkit.autopsy.core; +import java.util.Base64; +import java.util.prefs.BackingStoreException; +import org.sleuthkit.autopsy.events.MessageServiceConnectionInfo; import java.util.prefs.PreferenceChangeListener; import java.util.prefs.Preferences; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.PBEParameterSpec; +import org.openide.util.NbBundle; import org.openide.util.NbPreferences; +import org.sleuthkit.datamodel.CaseDbConnectionInfo; +import org.sleuthkit.datamodel.TskData.DbType; /** * Provides convenient access to a Preferences node for user preferences with @@ -34,11 +45,50 @@ public final class UserPreferences { public static final String HIDE_KNOWN_FILES_IN_VIEWS_TREE = "HideKnownFilesInViewsTree"; //NON-NLS public static final String DISPLAY_TIMES_IN_LOCAL_TIME = "DisplayTimesInLocalTime"; //NON-NLS public static final String NUMBER_OF_FILE_INGEST_THREADS = "NumberOfFileIngestThreads"; //NON-NLS + public static final String IS_MULTI_USER_MODE_ENABLED = "IsMultiUserModeEnabled"; //NON-NLS + public static final String EXTERNAL_DATABASE_HOSTNAME_OR_IP = "ExternalDatabaseHostnameOrIp"; //NON-NLS + public static final String EXTERNAL_DATABASE_PORTNUMBER = "ExternalDatabasePortNumber"; //NON-NLS + public static final String EXTERNAL_DATABASE_NAME = "ExternalDatabaseName"; //NON-NLS + public static final String EXTERNAL_DATABASE_USER = "ExternalDatabaseUsername"; //NON-NLS + public static final String EXTERNAL_DATABASE_PASSWORD = "ExternalDatabasePassword"; //NON-NLS + public static final String EXTERNAL_DATABASE_TYPE = "ExternalDatabaseType"; //NON-NLS + public static final String INDEXING_SERVER_HOST = "IndexingServerHost"; //NON-NLS + public static final String INDEXING_SERVER_PORT = "IndexingServerPort"; //NON-NLS + private static final String MESSAGE_SERVICE_PASSWORD = "MessageServicePassword"; //NON-NLS + private static final String MESSAGE_SERVICE_USER = "MessageServiceUser"; //NON-NLS + private static final String MESSAGE_SERVICE_HOST = "MessageServiceHost"; //NON-NLS + private static final String MESSAGE_SERVICE_PORT = "MessageServicePort"; //NON-NLS + public static final String PROCESS_TIME_OUT_ENABLED = "ProcessTimeOutEnabled"; //NON-NLS + public static final String PROCESS_TIME_OUT_HOURS = "ProcessTimeOutHours"; //NON-NLS + private static final int DEFAULT_PROCESS_TIMEOUT_HR = 60; + private static final String DEFAULT_PORT_STRING = "61616"; + private static final int DEFAULT_PORT_INT = 61616; // Prevent instantiation. private UserPreferences() { } + /** + * Reload all preferences from disk. This is only needed if the preferences + * file is being directly modified on disk while Autopsy is running. + * + * @throws BackingStoreException + */ + public static void reloadFromStorage() throws BackingStoreException { + preferences.sync(); + } + + /** + * Saves the current preferences to storage. This is only needed if the + * preferences files are going to be copied to another location while + * Autopsy is running. + * + * @throws BackingStoreException + */ + public static void saveToStorage() throws BackingStoreException { + preferences.flush(); + } + public static void addChangeListener(PreferenceChangeListener listener) { preferences.addPreferenceChangeListener(listener); } @@ -87,4 +137,209 @@ public final class UserPreferences { preferences.putInt(NUMBER_OF_FILE_INGEST_THREADS, value); } + /** + * Reads persisted case database connection info. + * @return An object encapsulating the database connection info. + * @throws org.sleuthkit.autopsy.core.UserPreferencesException + */ + public static CaseDbConnectionInfo getDatabaseConnectionInfo() throws UserPreferencesException { + DbType dbType; + try { + dbType = DbType.valueOf(preferences.get(EXTERNAL_DATABASE_TYPE, "POSTGRESQL")); + } catch (Exception ex) { + dbType = DbType.SQLITE; + } + return new CaseDbConnectionInfo( + preferences.get(EXTERNAL_DATABASE_HOSTNAME_OR_IP, ""), + preferences.get(EXTERNAL_DATABASE_PORTNUMBER, "5432"), + preferences.get(EXTERNAL_DATABASE_USER, ""), + TextConverter.convertHexTextToText(preferences.get(EXTERNAL_DATABASE_PASSWORD, "")), + dbType); + } + + /** + * Persists case database connection info. + * + * @param connectionInfo An object encapsulating the database connection + * info. + * @throws org.sleuthkit.autopsy.core.UserPreferencesException + */ + public static void setDatabaseConnectionInfo(CaseDbConnectionInfo connectionInfo) throws UserPreferencesException { + preferences.put(EXTERNAL_DATABASE_HOSTNAME_OR_IP, connectionInfo.getHost()); + preferences.put(EXTERNAL_DATABASE_PORTNUMBER, connectionInfo.getPort()); + preferences.put(EXTERNAL_DATABASE_USER, connectionInfo.getUserName()); + preferences.put(EXTERNAL_DATABASE_PASSWORD, TextConverter.convertTextToHexText(connectionInfo.getPassword())); + preferences.put(EXTERNAL_DATABASE_TYPE, connectionInfo.getDbType().toString()); + } + + public static void setIsMultiUserModeEnabled(boolean enabled) { + preferences.putBoolean(IS_MULTI_USER_MODE_ENABLED, enabled); + } + + public static boolean getIsMultiUserModeEnabled() { + return preferences.getBoolean(IS_MULTI_USER_MODE_ENABLED, false); + } + + public static String getIndexingServerHost() { + return preferences.get(INDEXING_SERVER_HOST, ""); + } + + public static void setIndexingServerHost(String hostName) { + preferences.put(INDEXING_SERVER_HOST, hostName); + } + + public static String getIndexingServerPort() { + return preferences.get(INDEXING_SERVER_PORT, "8983"); + } + + public static void setIndexingServerPort(int port) { + preferences.putInt(INDEXING_SERVER_PORT, port); + } + + /** + * Persists message service connection info. + * + * @param info An object encapsulating the message service info. + * @throws org.sleuthkit.autopsy.core.UserPreferencesException + */ + public static void setMessageServiceConnectionInfo(MessageServiceConnectionInfo info) throws UserPreferencesException { + preferences.put(MESSAGE_SERVICE_HOST, info.getHost()); + preferences.put(MESSAGE_SERVICE_PORT, Integer.toString(info.getPort())); + preferences.put(MESSAGE_SERVICE_USER, info.getUserName()); + preferences.put(MESSAGE_SERVICE_PASSWORD, TextConverter.convertTextToHexText(info.getPassword())); + } + + /** + * Reads persisted message service connection info. + * + * @return An object encapsulating the message service info. + * @throws org.sleuthkit.autopsy.core.UserPreferencesException + */ + public static MessageServiceConnectionInfo getMessageServiceConnectionInfo() throws UserPreferencesException { + int port; + try { + port = Integer.parseInt(preferences.get(MESSAGE_SERVICE_PORT, DEFAULT_PORT_STRING)); + } catch (NumberFormatException ex) { + // if there is an error parsing the port number, use the default port number + port = DEFAULT_PORT_INT; + } + + return new MessageServiceConnectionInfo( + preferences.get(MESSAGE_SERVICE_HOST, ""), + port, + preferences.get(MESSAGE_SERVICE_USER, ""), + TextConverter.convertHexTextToText(preferences.get(MESSAGE_SERVICE_PASSWORD, ""))); + } + + /** + * Reads persisted process time out value. + * + * @return int Process time out value (hours). + */ + public static int getProcessTimeOutHrs() { + int timeOut = preferences.getInt(PROCESS_TIME_OUT_HOURS, DEFAULT_PROCESS_TIMEOUT_HR); + if (timeOut < 0) { + timeOut = 0; + } + return timeOut; + } + + /** + * Stores persisted process time out value. + * + * @param value Persisted process time out value (hours). + */ + public static void setProcessTimeOutHrs(int value) { + if (value < 0) { + value = 0; + } + preferences.putInt(PROCESS_TIME_OUT_HOURS, value); + } + + /** + * Reads persisted setting of whether process time out functionality is + * enabled. + * + * @return boolean True if process time out is functionality enabled, false + * otherwise. + */ + public static boolean getIsTimeOutEnabled() { + boolean enabled = preferences.getBoolean(PROCESS_TIME_OUT_ENABLED, false); + return enabled; + } + + /** + * Stores persisted setting of whether process time out functionality is + * enabled. + * + * @param enabled Persisted setting of whether process time out + * functionality is enabled. + */ + public static void setIsTimeOutEnabled(boolean enabled) { + preferences.putBoolean(PROCESS_TIME_OUT_ENABLED, enabled); + } + + + /** + * Provides ability to convert text to hex text. + */ + static final class TextConverter { + + private static final char[] TMP = "hgleri21auty84fwe".toCharArray(); + private static final byte[] SALT = { + (byte) 0xde, (byte) 0x33, (byte) 0x10, (byte) 0x12, + (byte) 0xde, (byte) 0x33, (byte) 0x10, (byte) 0x12,}; + + /** + * Convert text to hex text. + * + * @param property Input text string. + * + * @return Converted hex string. + * + * @throws org.sleuthkit.autopsy.core.UserPreferencesException + */ + static String convertTextToHexText(String property) throws UserPreferencesException { + try { + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithMD5AndDES"); + SecretKey key = keyFactory.generateSecret(new PBEKeySpec(TMP)); + Cipher pbeCipher = Cipher.getInstance("PBEWithMD5AndDES"); + pbeCipher.init(Cipher.ENCRYPT_MODE, key, new PBEParameterSpec(SALT, 20)); + return base64Encode(pbeCipher.doFinal(property.getBytes("UTF-8"))); + } catch (Exception ex) { + throw new UserPreferencesException( + NbBundle.getMessage(TextConverter.class, "TextConverter.convert.exception.txt")); + } + } + + private static String base64Encode(byte[] bytes) { + return Base64.getEncoder().encodeToString(bytes); + } + + /** + * Convert hex text back to text. + * + * @param property Input hex text string. + * + * @return Converted text string. + * + * @throws org.sleuthkit.autopsy.core.UserPreferencesException + */ + static String convertHexTextToText(String property) throws UserPreferencesException { + try { + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithMD5AndDES"); + SecretKey key = keyFactory.generateSecret(new PBEKeySpec(TMP)); + Cipher pbeCipher = Cipher.getInstance("PBEWithMD5AndDES"); + pbeCipher.init(Cipher.DECRYPT_MODE, key, new PBEParameterSpec(SALT, 20)); + return new String(pbeCipher.doFinal(base64Decode(property)), "UTF-8"); + } catch (Exception ex) { + throw new UserPreferencesException( + NbBundle.getMessage(TextConverter.class, "TextConverter.convertFromHex.exception.txt")); + } + } + + private static byte[] base64Decode(String property) { + return Base64.getDecoder().decode(property); + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/events/ContentTagDeletedEvent.java b/Core/src/org/sleuthkit/autopsy/core/UserPreferencesException.java similarity index 60% rename from Core/src/org/sleuthkit/autopsy/events/ContentTagDeletedEvent.java rename to Core/src/org/sleuthkit/autopsy/core/UserPreferencesException.java index 6c6d11745f..7d9c22fe0b 100644 --- a/Core/src/org/sleuthkit/autopsy/events/ContentTagDeletedEvent.java +++ b/Core/src/org/sleuthkit/autopsy/core/UserPreferencesException.java @@ -16,19 +16,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.events; - -import javax.annotation.concurrent.Immutable; -import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.datamodel.ContentTag; +package org.sleuthkit.autopsy.core; /** - * An event that is fired when a ContentTag is deleted. + * Exception thrown when text conversion (such as from text to hex text or vice versa) resulted in + * an error */ -@Immutable -public class ContentTagDeletedEvent extends TagDeletedEvent { - - public ContentTagDeletedEvent(ContentTag deletedTag) { - super(Case.Events.CONTENT_TAG_DELETED.toString(), deletedTag); +public class UserPreferencesException extends Exception { + private static final long serialVersionUID = 1L; + + public UserPreferencesException(String message) { + super(message); } + + public UserPreferencesException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/Core/src/org/sleuthkit/autopsy/core/events/ServiceEvent.java b/Core/src/org/sleuthkit/autopsy/core/events/ServiceEvent.java new file mode 100644 index 0000000000..6b09f80908 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/core/events/ServiceEvent.java @@ -0,0 +1,49 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-2015 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.core.events; + +import java.io.Serializable; +import org.sleuthkit.autopsy.events.AutopsyEvent; + +/** + * A class for events to be published to registered subscribers of Service + * Monitor on this Autopsy node. The class extends PropertyChangeEvent (via + * AutopsyEvent) to integrate with legacy use of JavaBeans PropertyChangeEvents + * and PropertyChangeListeners as an application event system, and implements + * Serializable to allow it to be published over a network in serialized form. + */ +public final class ServiceEvent extends AutopsyEvent implements Serializable { + + private static final long serialVersionUID = 1L; + private final String details; + + public ServiceEvent(String serviceName, String status, String details) { + super(serviceName, null, status); + this.details = details; + } + + /** + * Gets details string passed as input to ServiceEvent constructor. + * + * @return String Details of the event. + */ + public String getDetails() { + return details; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/core/layer.xml b/Core/src/org/sleuthkit/autopsy/core/layer.xml index 2f36280e7a..4c4b88125a 100644 --- a/Core/src/org/sleuthkit/autopsy/core/layer.xml +++ b/Core/src/org/sleuthkit/autopsy/core/layer.xml @@ -19,7 +19,7 @@ - + @@ -44,7 +44,6 @@ - @@ -203,6 +202,13 @@ + + + + + + + @@ -215,6 +221,8 @@ --> + + @@ -227,6 +235,7 @@ + diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/AboutWindowPanel.java b/Core/src/org/sleuthkit/autopsy/corecomponents/AboutWindowPanel.java index abba7ce94b..640cc45409 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/AboutWindowPanel.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/AboutWindowPanel.java @@ -26,7 +26,7 @@ import java.net.URL; import java.text.MessageFormat; import java.util.Locale; import java.util.logging.Level; -import java.util.logging.Logger; +import org.sleuthkit.autopsy.coreutils.Logger; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JPanel; diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanel.form b/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanel.form index 153a137748..14f2a4f112 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanel.form @@ -6,8 +6,6 @@ - - @@ -58,6 +56,20 @@ + + + + + + + + + + + + + + @@ -89,7 +101,17 @@ - + + + + + + + + + + + @@ -205,5 +227,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanel.java b/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanel.java index e683de4c0d..9fb5b41089 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanel.java @@ -18,7 +18,9 @@ */ package org.sleuthkit.autopsy.corecomponents; +import java.text.NumberFormat; import javax.swing.DefaultComboBoxModel; +import javax.swing.JFormattedTextField; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.core.UserPreferences; @@ -81,6 +83,19 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { useLocalTimeRB.setSelected(useLocalTime); useGMTTimeRB.setSelected(!useLocalTime); numberOfFileIngestThreadsComboBox.setSelectedItem(UserPreferences.numberOfFileIngestThreads()); + if (UserPreferences.getIsTimeOutEnabled()) { + // user specified time out + jCheckBoxEnableProcTimeout.setSelected(true); + jFormattedTextFieldProcTimeOutHrs.setEditable(true); + int timeOutHrs = UserPreferences.getProcessTimeOutHrs(); + jFormattedTextFieldProcTimeOutHrs.setValue((long) timeOutHrs); + } else { + // never time out + jCheckBoxEnableProcTimeout.setSelected(false); + jFormattedTextFieldProcTimeOutHrs.setEditable(false); + int timeOutHrs = UserPreferences.getProcessTimeOutHrs(); + jFormattedTextFieldProcTimeOutHrs.setValue((long) timeOutHrs); + } } void store() { @@ -89,6 +104,13 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { UserPreferences.setHideKnownFilesInViewsTree(viewsHideKnownCB.isSelected()); UserPreferences.setDisplayTimesInLocalTime(useLocalTimeRB.isSelected()); UserPreferences.setNumberOfFileIngestThreads((Integer) numberOfFileIngestThreadsComboBox.getSelectedItem()); + + UserPreferences.setIsTimeOutEnabled(jCheckBoxEnableProcTimeout.isSelected()); + if (jCheckBoxEnableProcTimeout.isSelected()) { + // only store time out if it is enabled + long timeOutHrs = (long) jFormattedTextFieldProcTimeOutHrs.getValue(); + UserPreferences.setProcessTimeOutHrs((int) timeOutHrs); + } } boolean valid() { @@ -105,7 +127,6 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { buttonGroup1 = new javax.swing.ButtonGroup(); buttonGroup3 = new javax.swing.ButtonGroup(); - buttonGroupProcTimeOut = new javax.swing.ButtonGroup(); useBestViewerRB = new javax.swing.JRadioButton(); keepCurrentViewerRB = new javax.swing.JRadioButton(); jLabelSelectFile = new javax.swing.JLabel(); @@ -118,6 +139,10 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { jLabelNumThreads = new javax.swing.JLabel(); numberOfFileIngestThreadsComboBox = new javax.swing.JComboBox(); restartRequiredLabel = new javax.swing.JLabel(); + jLabelSetProcessTimeOut = new javax.swing.JLabel(); + jCheckBoxEnableProcTimeout = new javax.swing.JCheckBox(); + jLabelProcessTimeOutUnits = new javax.swing.JLabel(); + jFormattedTextFieldProcTimeOutHrs = new JFormattedTextField(NumberFormat.getIntegerInstance()); buttonGroup1.add(useBestViewerRB); useBestViewerRB.setSelected(true); @@ -150,6 +175,19 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { restartRequiredLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/corecomponents/warning16.png"))); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(restartRequiredLabel, org.openide.util.NbBundle.getMessage(AutopsyOptionsPanel.class, "AutopsyOptionsPanel.restartRequiredLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(jLabelSetProcessTimeOut, org.openide.util.NbBundle.getMessage(AutopsyOptionsPanel.class, "AutopsyOptionsPanel.jLabelSetProcessTimeOut.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(jCheckBoxEnableProcTimeout, org.openide.util.NbBundle.getMessage(AutopsyOptionsPanel.class, "AutopsyOptionsPanel.jCheckBoxEnableProcTimeout.text")); // NOI18N + jCheckBoxEnableProcTimeout.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + jCheckBoxEnableProcTimeoutActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(jLabelProcessTimeOutUnits, org.openide.util.NbBundle.getMessage(AutopsyOptionsPanel.class, "AutopsyOptionsPanel.jLabelProcessTimeOutUnits.text")); // NOI18N + + jFormattedTextFieldProcTimeOutHrs.setText(org.openide.util.NbBundle.getMessage(AutopsyOptionsPanel.class, "AutopsyOptionsPanel.jFormattedTextFieldProcTimeOutHrs.text")); // NOI18N + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( @@ -180,7 +218,18 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { .addComponent(useGMTTimeRB))) .addComponent(jLabelSelectFile) .addComponent(jLabelNumThreads)) - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)))) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(10, 10, 10) + .addComponent(jCheckBoxEnableProcTimeout) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jFormattedTextFieldProcTimeOutHrs, javax.swing.GroupLayout.PREFERRED_SIZE, 27, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabelProcessTimeOutUnits)) + .addComponent(jLabelSetProcessTimeOut)) + .addGap(0, 0, Short.MAX_VALUE)))) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) @@ -208,18 +257,33 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(numberOfFileIngestThreadsComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(restartRequiredLabel)) - .addContainerGap(155, Short.MAX_VALUE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(jLabelSetProcessTimeOut) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jCheckBoxEnableProcTimeout) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(jFormattedTextFieldProcTimeOutHrs, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(jLabelProcessTimeOutUnits))) + .addContainerGap(103, Short.MAX_VALUE)) ); }// //GEN-END:initComponents + private void jCheckBoxEnableProcTimeoutActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jCheckBoxEnableProcTimeoutActionPerformed + jFormattedTextFieldProcTimeOutHrs.setEditable(jCheckBoxEnableProcTimeout.isSelected()); + }//GEN-LAST:event_jCheckBoxEnableProcTimeoutActionPerformed + // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.ButtonGroup buttonGroup1; private javax.swing.ButtonGroup buttonGroup3; - private javax.swing.ButtonGroup buttonGroupProcTimeOut; private javax.swing.JCheckBox dataSourcesHideKnownCB; + private javax.swing.JCheckBox jCheckBoxEnableProcTimeout; + private javax.swing.JFormattedTextField jFormattedTextFieldProcTimeOutHrs; private javax.swing.JLabel jLabelHideKnownFiles; private javax.swing.JLabel jLabelNumThreads; + private javax.swing.JLabel jLabelProcessTimeOutUnits; private javax.swing.JLabel jLabelSelectFile; + private javax.swing.JLabel jLabelSetProcessTimeOut; private javax.swing.JLabel jLabelTimeDisplay; private javax.swing.JRadioButton keepCurrentViewerRB; private javax.swing.JComboBox numberOfFileIngestThreadsComboBox; diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanelController.java b/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanelController.java index c0de3942ea..9253da5df5 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanelController.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanelController.java @@ -30,7 +30,7 @@ import java.util.logging.Level; import org.sleuthkit.autopsy.coreutils.Logger; @OptionsPanelController.TopLevelRegistration(categoryName = "#OptionsCategory_Name_General", - iconBase = "org/sleuthkit/autopsy/corecomponents/checkbox.png", + iconBase = "org/sleuthkit/autopsy/corecomponents/checkbox32.png", position = 1, keywords = "#OptionsCategory_Keywords_General", keywordsCategory = "General") diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties index caa5ecd386..06146d2d8f 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties @@ -147,7 +147,52 @@ AutopsyOptionsPanel.jLabelNumThreads.text=Number of threads to use for file inge FXVideoPanel.progress.bufferingCancelled=media buffering was canceled FXVideoPanel.progress.bufferingInterrupted=media buffering was interrupted FXVideoPanel.progress.errorWritingVideoToDisk=Error writing video to disk +OptionsCategory_Name_Multi_User_Settings=Multi-user +OptionsCategory_Keywords_Multi_User_Options=Multi-user Options +MultiUserSettingsPanel.lbSolrSettings.text=Solr Settings +MultiUserSettingsPanel.cbEnableMultiUser.text=Enable Multi-user cases +MultiUserSettingsPanel.lbDatabaseSettings.text=Database Settings +MultiUserSettingsPanel.validationErrMsg.incomplete=Fill in all values +MultiUserSettingsPanel.validationErrMsg.invalidDatabasePort=Invalid database port number +MultiUserSettingsPanel.validationErrMsg.invalidMessageServicePort=Invalid message service port number +MultiUserSettingsPanel.validationErrMsg.invalidIndexingServerPort=Invalid Solr server port number +MultiUserSettingsPanel.validationErrMsg.invalidMessgeServiceURI=Message service host and/or port not valid +AutopsyOptionsPanel.jCheckBoxEnableProcTimeout.text= +AutopsyOptionsPanel.jFormattedTextFieldProcTimeOutHrs.text=60 +AutopsyOptionsPanel.jLabelProcessTimeOutUnits.text=hour(s) +AutopsyOptionsPanel.jLabelSetProcessTimeOut.text=Enable timeout to allow modules to automatically terminate after a set amount of time: DataContentViewerHex.goToOffsetLabel.text=Jump to Offset DataContentViewerHex.goToOffsetTextField.text= DataContentViewerHex.goToOffsetTextField.msgDlg=Invalid Offset: {0} -DataContentViewerHex.setDataView.invalidOffset.negativeOffsetValue=Cannot jump to the resultant offset \ No newline at end of file +DataContentViewerHex.setDataView.invalidOffset.negativeOffsetValue=Cannot jump to the resultant offset +MultiUserSettingsPanel.tbOops.text= +MultiUserSettingsPanel.lbTestDatabase.text= +MultiUserSettingsPanel.bnTestDatabase.text=Test +MultiUserSettingsPanel.tbDbHostname.toolTipText=Hostname or IP Address +MultiUserSettingsPanel.tbDbHostname.text= +MultiUserSettingsPanel.tbDbPort.toolTipText=Port Number +MultiUserSettingsPanel.tbDbPort.text= +MultiUserSettingsPanel.tbDbUsername.toolTipText=User Name +MultiUserSettingsPanel.tbDbUsername.text= +MultiUserSettingsPanel.tbDbPassword.toolTipText=Password +MultiUserSettingsPanel.tbDbPassword.text= +MultiUserSettingsPanel.lbTestSolr.text= +MultiUserSettingsPanel.bnTestSolr.text=Test +MultiUserSettingsPanel.tbSolrHostname.toolTipText=Hostname or IP Address +MultiUserSettingsPanel.tbSolrPort.toolTipText=Port Number +MultiUserSettingsPanel.lbTestMessageService.text= +MultiUserSettingsPanel.bnTestMessageService.text=Test +MultiUserSettingsPanel.lbMessageServiceSettings.text=ActiveMQ Message Service Settings +MultiUserSettingsPanel.tbMsgPort.toolTipText=Port Number +MultiUserSettingsPanel.tbMsgPort.text= +MultiUserSettingsPanel.tbMsgUsername.toolTipText=User Name +MultiUserSettingsPanel.tbMsgUsername.text= +MultiUserSettingsPanel.tbMsgPassword.toolTipText=Password +MultiUserSettingsPanel.tbMsgPassword.text= +MultiUserSettingsPanel.tbMsgHostname.toolTipText=Hostname or IP Address +MultiUserSettingsPanel.tbMsgHostname.text= +MultiUserSettingsPanel.lbTestMessageWarning.text= +MultiUserSettingsPanel.lbTestSolrWarning.text= +MultiUserSettingsPanel.lbTestDbWarning.text= +MultiUserSettingsPanel.KeywordSearchNull=Cannot find Keyword Search service +MultiUserSettingsPanel.InvalidPortNumber=Invalid port number. \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerMedia.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerMedia.java index c3749660bf..9c1efe9b21 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerMedia.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerMedia.java @@ -171,7 +171,7 @@ public class DataContentViewerMedia extends javax.swing.JPanel implements DataCo } /** - * is the given file a video we can display? + * Is the given file a video we can display? * * @param file * @@ -185,14 +185,13 @@ public class DataContentViewerMedia extends javax.swing.JPanel implements DataCo } /** - * is the given file an image that we can display? + * Is the given file an image that we can display? * * @param file * * @return True if an image file that can be displayed */ private boolean isImageSupported(AbstractFile file) { - if (null == file || file.getSize() == 0) { return false; } diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.form b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.form index e86a2a5daf..f471d230a7 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.form +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.form @@ -45,11 +45,7 @@ - - - - - + diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.java index f1e790f0f8..4f3910357d 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.java @@ -578,6 +578,5 @@ final class DataResultViewerThumbnail extends AbstractDataResultViewer { } } } - } } diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/Installer.java b/Core/src/org/sleuthkit/autopsy/corecomponents/Installer.java index 66a672c16b..c2e989635e 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/Installer.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/Installer.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011 Basis Technology Corp. + * Copyright 2011-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -69,25 +69,26 @@ public class Installer extends ModuleInstall { /* * Open the passed in case, if an aut file was double clicked. */ - WindowManager.getDefault().invokeWhenUIReady(new Runnable() { - @Override - public void run() { - Collection processors = Lookup.getDefault().lookupAll(OptionProcessor.class); - for (OptionProcessor processor : processors) { - if (processor instanceof OpenFromArguments) { - OpenFromArguments argsProcessor = (OpenFromArguments) processor; - String caseFile = argsProcessor.getDefaultArg(); - if (caseFile != null && !caseFile.equals("") && caseFile.endsWith(".aut") && new File(caseFile).exists()) { //NON-NLS + WindowManager.getDefault().invokeWhenUIReady(() -> { + Collection processors = Lookup.getDefault().lookupAll(OptionProcessor.class); + for (OptionProcessor processor : processors) { + if (processor instanceof OpenFromArguments) { + OpenFromArguments argsProcessor = (OpenFromArguments) processor; + final String caseFile = argsProcessor.getDefaultArg(); + if (caseFile != null && !caseFile.equals("") && caseFile.endsWith(".aut") && new File(caseFile).exists()) { //NON-NLS + new Thread(() -> { + // Create case. try { Case.open(caseFile); - return; - } catch (Exception e) { + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error opening case: ", ex); //NON-NLS } - } + }).start(); + return; } } - Case.invokeStartupDialog(); // bring up the startup dialog } + Case.invokeStartupDialog(); // bring up the startup dialog }); } @@ -95,18 +96,19 @@ public class Installer extends ModuleInstall { @Override public void uninstalled() { super.uninstalled(); - } @Override public void close() { - try { - if (Case.isCaseOpen()) { - Case.getCurrentCase().closeCase(); + new Thread(() -> { + try { + if (Case.isCaseOpen()) { + Case.getCurrentCase().closeCase(); + } + } catch (CaseActionException | IllegalStateException unused) { + // Exception already logged. Shutting down, no need to do popup. } - } catch (CaseActionException ex) { - logger.log(Level.WARNING, "Error closing case. ", ex); //NON-NLS - } + }).start(); } private void setupLAF() { @@ -157,8 +159,8 @@ public class Installer extends ModuleInstall { } // Overwrite the Metal menu item keys to use the Aqua versions - for (Map.Entry entry : uiEntries.entrySet()) { + uiEntries.entrySet().stream().forEach((entry) -> { UIManager.put(entry.getKey(), entry.getValue()); - } + }); } } diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java b/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java index 15997146ea..6872aace78 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/MediaViewImagePanel.java @@ -239,5 +239,4 @@ public class MediaViewImagePanel extends JPanel implements DataContentViewerMedi }// //GEN-END:initComponents // Variables declaration - do not modify//GEN-BEGIN:variables // End of variables declaration//GEN-END:variables - } diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/MultiUserSettingsPanel.form b/Core/src/org/sleuthkit/autopsy/corecomponents/MultiUserSettingsPanel.form new file mode 100644 index 0000000000..57f1eca7d5 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/MultiUserSettingsPanel.form @@ -0,0 +1,520 @@ + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/MultiUserSettingsPanel.java b/Core/src/org/sleuthkit/autopsy/corecomponents/MultiUserSettingsPanel.java new file mode 100644 index 0000000000..0a26244edc --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/MultiUserSettingsPanel.java @@ -0,0 +1,862 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.sleuthkit.autopsy.corecomponents; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.Collection; +import javax.swing.JTextField; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import org.openide.util.NbBundle; +import org.sleuthkit.datamodel.CaseDbConnectionInfo; +import org.sleuthkit.datamodel.TskData.DbType; +import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.autopsy.events.MessageServiceConnectionInfo; +import org.sleuthkit.autopsy.coreutils.Logger; +import java.awt.Cursor; +import java.util.logging.Level; +import javax.swing.ImageIcon; +import org.openide.util.ImageUtilities; +import org.openide.util.Lookup; +import org.sleuthkit.autopsy.core.UserPreferencesException; +import org.sleuthkit.autopsy.events.MessageServiceException; +import org.sleuthkit.autopsy.keywordsearchservice.KeywordSearchService; +import org.sleuthkit.autopsy.keywordsearchservice.KeywordSearchServiceException; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +public final class MultiUserSettingsPanel extends javax.swing.JPanel { + + private static final String HOST_NAME_OR_IP_PROMPT = NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.tbDbHostname.toolTipText"); + private static final String PORT_PROMPT = NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.tbDbPort.toolTipText"); + private static final String USER_NAME_PROMPT = NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.tbDbUsername.toolTipText"); + private static final String PASSWORD_PROMPT = NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.tbDbPassword.toolTipText"); + private static final String INCOMPLETE_SETTINGS_MSG = NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.validationErrMsg.incomplete"); + private static final String INVALID_DB_PORT_MSG = NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.validationErrMsg.invalidDatabasePort"); + private static final String INVALID_MESSAGE_SERVICE_PORT_MSG = NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.validationErrMsg.invalidMessageServicePort"); + private static final String INVALID_INDEXING_SERVER_PORT_MSG = NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.validationErrMsg.invalidIndexingServerPort"); + private static final int DEFAULT_MESSAGE_SERVICE_PORT = 61616; + private static final long serialVersionUID = 1L; + private final MultiUserSettingsPanelController controller; + private final Collection textBoxes = new ArrayList<>(); + private final TextBoxChangedListener textBoxChangedListener; + private static final Logger logger = Logger.getLogger(MultiUserSettingsPanel.class.getName()); + private final ImageIcon goodIcon; + private final ImageIcon badIcon; + + /** + * Creates new form AutopsyMultiUserSettingsPanel + * + * @param theController Controller to notify of changes. + */ + public MultiUserSettingsPanel(MultiUserSettingsPanelController theController) { + initComponents(); + controller = theController; + setSize(555, 600); + + /** + * Add text prompts to all of the text fields. + */ + Collection textPrompts = new ArrayList<>(); + textPrompts.add(new TextPrompt(HOST_NAME_OR_IP_PROMPT, tbDbHostname)); + textPrompts.add(new TextPrompt(PORT_PROMPT, tbDbPort)); + textPrompts.add(new TextPrompt(USER_NAME_PROMPT, tbDbUsername)); + textPrompts.add(new TextPrompt(PASSWORD_PROMPT, tbDbPassword)); + textPrompts.add(new TextPrompt(HOST_NAME_OR_IP_PROMPT, tbMsgHostname)); + textPrompts.add(new TextPrompt(PORT_PROMPT, tbMsgPort)); + textPrompts.add(new TextPrompt(USER_NAME_PROMPT, tbMsgUsername)); + textPrompts.add(new TextPrompt(PASSWORD_PROMPT, tbMsgPassword)); + textPrompts.add(new TextPrompt(HOST_NAME_OR_IP_PROMPT, tbSolrHostname)); + textPrompts.add(new TextPrompt(PORT_PROMPT, tbSolrPort)); + configureTextPrompts(textPrompts); + + /* Set each textbox with a "statusIcon" property enabling the + DocumentListeners to know which icon to erase when changes are made */ + tbDbHostname.getDocument().putProperty("statusIcon", lbTestDatabase); + tbDbPort.getDocument().putProperty("statusIcon", lbTestDatabase); + tbDbUsername.getDocument().putProperty("statusIcon", lbTestDatabase); + tbDbPassword.getDocument().putProperty("statusIcon", lbTestDatabase); + + tbSolrHostname.getDocument().putProperty("statusIcon", lbTestSolr); + tbSolrPort.getDocument().putProperty("statusIcon", lbTestSolr); + + tbMsgHostname.getDocument().putProperty("statusIcon", lbTestMessageService); + tbMsgPort.getDocument().putProperty("statusIcon", lbTestMessageService); + tbMsgUsername.getDocument().putProperty("statusIcon", lbTestMessageService); + tbMsgPassword.getDocument().putProperty("statusIcon", lbTestMessageService); + + /// Register for notifications when the text boxes get updated. + textBoxChangedListener = new TextBoxChangedListener(); + textBoxes.add(tbDbHostname); + textBoxes.add(tbDbPort); + textBoxes.add(tbDbUsername); + textBoxes.add(tbDbPassword); + textBoxes.add(tbMsgHostname); + textBoxes.add(tbMsgPort); + textBoxes.add(tbMsgUsername); + textBoxes.add(tbMsgPassword); + textBoxes.add(tbSolrHostname); + textBoxes.add(tbSolrPort); + + addDocumentListeners(textBoxes, textBoxChangedListener); + goodIcon = new ImageIcon(ImageUtilities.loadImage("org/sleuthkit/autopsy/images/good.png", false)); + badIcon = new ImageIcon(ImageUtilities.loadImage("org/sleuthkit/autopsy/images/bad.png", false)); + enableMultiUserComponents(textBoxes, cbEnableMultiUser.isSelected()); + } + + /** + * Sets the foreground color and transparency of a collection of text + * prompts. + * + * @param textPrompts The text prompts to configure. + */ + private static void configureTextPrompts(Collection textPrompts) { + float alpha = 0.9f; // Mostly opaque + for (TextPrompt textPrompt : textPrompts) { + textPrompt.setForeground(Color.LIGHT_GRAY); + textPrompt.changeAlpha(alpha); + } + } + + /** + * Adds a change listener to a collection of text fields. + * + * @param textFields The text fields. + * @param listener The change listener. + */ + private static void addDocumentListeners(Collection textFields, TextBoxChangedListener listener) { + for (JTextField textField : textFields) { + textField.getDocument().addDocumentListener(listener); + } + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + pnOverallPanel = new javax.swing.JPanel(); + pnDatabaseSettings = new javax.swing.JPanel(); + tbDbHostname = new javax.swing.JTextField(); + tbDbPort = new javax.swing.JTextField(); + tbDbUsername = new javax.swing.JTextField(); + tbDbPassword = new javax.swing.JPasswordField(); + lbDatabaseSettings = new javax.swing.JLabel(); + bnTestDatabase = new javax.swing.JButton(); + lbTestDatabase = new javax.swing.JLabel(); + lbTestDbWarning = new javax.swing.JLabel(); + pnSolrSettings = new javax.swing.JPanel(); + lbSolrSettings = new javax.swing.JLabel(); + tbSolrHostname = new javax.swing.JTextField(); + tbSolrPort = new javax.swing.JTextField(); + bnTestSolr = new javax.swing.JButton(); + lbTestSolr = new javax.swing.JLabel(); + lbTestSolrWarning = new javax.swing.JLabel(); + pnMessagingSettings = new javax.swing.JPanel(); + lbMessageServiceSettings = new javax.swing.JLabel(); + tbMsgHostname = new javax.swing.JTextField(); + tbMsgUsername = new javax.swing.JTextField(); + tbMsgPort = new javax.swing.JTextField(); + tbMsgPassword = new javax.swing.JPasswordField(); + bnTestMessageService = new javax.swing.JButton(); + lbTestMessageService = new javax.swing.JLabel(); + lbTestMessageWarning = new javax.swing.JLabel(); + cbEnableMultiUser = new javax.swing.JCheckBox(); + tbOops = new javax.swing.JTextField(); + + pnDatabaseSettings.setBorder(javax.swing.BorderFactory.createEtchedBorder()); + + tbDbHostname.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + tbDbHostname.setText(org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.tbDbHostname.text")); // NOI18N + tbDbHostname.setToolTipText(org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.tbDbHostname.toolTipText")); // NOI18N + + tbDbPort.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + tbDbPort.setText(org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.tbDbPort.text")); // NOI18N + tbDbPort.setToolTipText(org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.tbDbPort.toolTipText")); // NOI18N + + tbDbUsername.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + tbDbUsername.setText(org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.tbDbUsername.text")); // NOI18N + tbDbUsername.setToolTipText(org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.tbDbUsername.toolTipText")); // NOI18N + + tbDbPassword.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + tbDbPassword.setText(org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.tbDbPassword.text")); // NOI18N + tbDbPassword.setToolTipText(org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.tbDbPassword.toolTipText")); // NOI18N + + lbDatabaseSettings.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(lbDatabaseSettings, org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.lbDatabaseSettings.text")); // NOI18N + lbDatabaseSettings.setVerticalAlignment(javax.swing.SwingConstants.TOP); + + org.openide.awt.Mnemonics.setLocalizedText(bnTestDatabase, org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.bnTestDatabase.text")); // NOI18N + bnTestDatabase.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnTestDatabaseActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(lbTestDatabase, org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.lbTestDatabase.text")); // NOI18N + lbTestDatabase.setAutoscrolls(true); + + lbTestDbWarning.setForeground(new java.awt.Color(255, 0, 0)); + org.openide.awt.Mnemonics.setLocalizedText(lbTestDbWarning, org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.lbTestDbWarning.text")); // NOI18N + + javax.swing.GroupLayout pnDatabaseSettingsLayout = new javax.swing.GroupLayout(pnDatabaseSettings); + pnDatabaseSettings.setLayout(pnDatabaseSettingsLayout); + pnDatabaseSettingsLayout.setHorizontalGroup( + pnDatabaseSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(pnDatabaseSettingsLayout.createSequentialGroup() + .addContainerGap() + .addGroup(pnDatabaseSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(tbDbHostname) + .addGroup(pnDatabaseSettingsLayout.createSequentialGroup() + .addComponent(lbDatabaseSettings) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(bnTestDatabase) + .addGap(18, 18, 18) + .addComponent(lbTestDatabase, javax.swing.GroupLayout.PREFERRED_SIZE, 16, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(tbDbPort) + .addComponent(tbDbUsername) + .addComponent(tbDbPassword) + .addGroup(pnDatabaseSettingsLayout.createSequentialGroup() + .addComponent(lbTestDbWarning) + .addGap(0, 0, Short.MAX_VALUE))) + .addContainerGap()) + ); + pnDatabaseSettingsLayout.setVerticalGroup( + pnDatabaseSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, pnDatabaseSettingsLayout.createSequentialGroup() + .addContainerGap() + .addGroup(pnDatabaseSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(bnTestDatabase) + .addComponent(lbTestDatabase, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(lbDatabaseSettings)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(tbDbHostname, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(tbDbPort, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(tbDbUsername, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(tbDbPassword, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(lbTestDbWarning, javax.swing.GroupLayout.PREFERRED_SIZE, 16, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap()) + ); + + pnSolrSettings.setBorder(javax.swing.BorderFactory.createEtchedBorder()); + + lbSolrSettings.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(lbSolrSettings, org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.lbSolrSettings.text")); // NOI18N + + tbSolrHostname.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + tbSolrHostname.setToolTipText(org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.tbSolrHostname.toolTipText")); // NOI18N + + tbSolrPort.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + tbSolrPort.setToolTipText(org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.tbSolrPort.toolTipText")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(bnTestSolr, org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.bnTestSolr.text")); // NOI18N + bnTestSolr.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnTestSolrActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(lbTestSolr, org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.lbTestSolr.text")); // NOI18N + + lbTestSolrWarning.setForeground(new java.awt.Color(255, 0, 0)); + org.openide.awt.Mnemonics.setLocalizedText(lbTestSolrWarning, org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.lbTestSolrWarning.text")); // NOI18N + + javax.swing.GroupLayout pnSolrSettingsLayout = new javax.swing.GroupLayout(pnSolrSettings); + pnSolrSettings.setLayout(pnSolrSettingsLayout); + pnSolrSettingsLayout.setHorizontalGroup( + pnSolrSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(pnSolrSettingsLayout.createSequentialGroup() + .addContainerGap() + .addGroup(pnSolrSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(tbSolrHostname) + .addGroup(pnSolrSettingsLayout.createSequentialGroup() + .addComponent(lbSolrSettings) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(bnTestSolr) + .addGap(18, 18, 18) + .addComponent(lbTestSolr, javax.swing.GroupLayout.PREFERRED_SIZE, 16, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(tbSolrPort) + .addGroup(pnSolrSettingsLayout.createSequentialGroup() + .addComponent(lbTestSolrWarning) + .addGap(0, 0, Short.MAX_VALUE))) + .addContainerGap()) + ); + pnSolrSettingsLayout.setVerticalGroup( + pnSolrSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(pnSolrSettingsLayout.createSequentialGroup() + .addContainerGap() + .addGroup(pnSolrSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(pnSolrSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(bnTestSolr, javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(lbSolrSettings)) + .addComponent(lbTestSolr, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(tbSolrHostname, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(tbSolrPort, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(lbTestSolrWarning, javax.swing.GroupLayout.PREFERRED_SIZE, 16, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap()) + ); + + pnMessagingSettings.setBorder(javax.swing.BorderFactory.createEtchedBorder()); + + lbMessageServiceSettings.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(lbMessageServiceSettings, org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.lbMessageServiceSettings.text")); // NOI18N + + tbMsgHostname.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + tbMsgHostname.setText(org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.tbMsgHostname.text")); // NOI18N + tbMsgHostname.setToolTipText(org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.tbMsgHostname.toolTipText")); // NOI18N + + tbMsgUsername.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + tbMsgUsername.setText(org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.tbMsgUsername.text")); // NOI18N + tbMsgUsername.setToolTipText(org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.tbMsgUsername.toolTipText")); // NOI18N + + tbMsgPort.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + tbMsgPort.setText(org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.tbMsgPort.text")); // NOI18N + tbMsgPort.setToolTipText(org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.tbMsgPort.toolTipText")); // NOI18N + + tbMsgPassword.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + tbMsgPassword.setText(org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.tbMsgPassword.text")); // NOI18N + tbMsgPassword.setToolTipText(org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.tbMsgPassword.toolTipText")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(bnTestMessageService, org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.bnTestMessageService.text")); // NOI18N + bnTestMessageService.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnTestMessageServiceActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(lbTestMessageService, org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.lbTestMessageService.text")); // NOI18N + + lbTestMessageWarning.setForeground(new java.awt.Color(255, 0, 0)); + org.openide.awt.Mnemonics.setLocalizedText(lbTestMessageWarning, org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.lbTestMessageWarning.text")); // NOI18N + + javax.swing.GroupLayout pnMessagingSettingsLayout = new javax.swing.GroupLayout(pnMessagingSettings); + pnMessagingSettings.setLayout(pnMessagingSettingsLayout); + pnMessagingSettingsLayout.setHorizontalGroup( + pnMessagingSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(pnMessagingSettingsLayout.createSequentialGroup() + .addContainerGap() + .addGroup(pnMessagingSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(tbMsgHostname) + .addGroup(pnMessagingSettingsLayout.createSequentialGroup() + .addComponent(lbMessageServiceSettings) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 229, Short.MAX_VALUE) + .addComponent(bnTestMessageService) + .addGap(18, 18, 18) + .addComponent(lbTestMessageService, javax.swing.GroupLayout.PREFERRED_SIZE, 16, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(tbMsgPort) + .addComponent(tbMsgUsername) + .addComponent(tbMsgPassword) + .addGroup(pnMessagingSettingsLayout.createSequentialGroup() + .addComponent(lbTestMessageWarning) + .addGap(0, 0, Short.MAX_VALUE))) + .addContainerGap()) + ); + pnMessagingSettingsLayout.setVerticalGroup( + pnMessagingSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(pnMessagingSettingsLayout.createSequentialGroup() + .addContainerGap() + .addGroup(pnMessagingSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addGroup(pnMessagingSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(bnTestMessageService, javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(lbMessageServiceSettings)) + .addComponent(lbTestMessageService, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(tbMsgHostname, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(tbMsgPort, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(tbMsgUsername, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(tbMsgPassword, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(lbTestMessageWarning, javax.swing.GroupLayout.PREFERRED_SIZE, 16, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + + org.openide.awt.Mnemonics.setLocalizedText(cbEnableMultiUser, org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.cbEnableMultiUser.text")); // NOI18N + cbEnableMultiUser.addItemListener(new java.awt.event.ItemListener() { + public void itemStateChanged(java.awt.event.ItemEvent evt) { + cbEnableMultiUserItemStateChanged(evt); + } + }); + + tbOops.setEditable(false); + tbOops.setFont(new java.awt.Font("Tahoma", 1, 12)); // NOI18N + tbOops.setForeground(new java.awt.Color(255, 0, 0)); + tbOops.setText(org.openide.util.NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.tbOops.text")); // NOI18N + tbOops.setBorder(null); + + javax.swing.GroupLayout pnOverallPanelLayout = new javax.swing.GroupLayout(pnOverallPanel); + pnOverallPanel.setLayout(pnOverallPanelLayout); + pnOverallPanelLayout.setHorizontalGroup( + pnOverallPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, pnOverallPanelLayout.createSequentialGroup() + .addContainerGap() + .addGroup(pnOverallPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addGroup(pnOverallPanelLayout.createSequentialGroup() + .addComponent(cbEnableMultiUser) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(tbOops)) + .addComponent(pnSolrSettings, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(pnDatabaseSettings, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(pnMessagingSettings, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addContainerGap()) + ); + pnOverallPanelLayout.setVerticalGroup( + pnOverallPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, pnOverallPanelLayout.createSequentialGroup() + .addGroup(pnOverallPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.CENTER) + .addComponent(tbOops, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(cbEnableMultiUser)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(pnDatabaseSettings, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(pnSolrSettings, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(pnMessagingSettings, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap(39, Short.MAX_VALUE)) + ); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(pnOverallPanel, javax.swing.GroupLayout.PREFERRED_SIZE, 555, javax.swing.GroupLayout.PREFERRED_SIZE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(pnOverallPanel, javax.swing.GroupLayout.PREFERRED_SIZE, 559, javax.swing.GroupLayout.PREFERRED_SIZE) + ); + }// //GEN-END:initComponents + + /** + * Enables/disables the multi-user settings, based upon input provided + * + * @param enabled true means enable, false means disable + */ + private static void enableMultiUserComponents(Collection textFields, boolean enabled) { + for (JTextField textField : textFields) { + textField.setEnabled(enabled); + } + } + + private void cbEnableMultiUserItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_cbEnableMultiUserItemStateChanged + if (!cbEnableMultiUser.isSelected()) { + tbOops.setText(""); + bnTestDatabase.setEnabled(false); + lbTestDatabase.setIcon(null); + bnTestSolr.setEnabled(false); + lbTestSolr.setIcon(null); + bnTestMessageService.setEnabled(false); + lbTestMessageService.setIcon(null); + lbTestDbWarning.setText(""); + lbTestSolrWarning.setText(""); + lbTestMessageWarning.setText(""); + } + enableMultiUserComponents(textBoxes, cbEnableMultiUser.isSelected()); + controller.changed(); + }//GEN-LAST:event_cbEnableMultiUserItemStateChanged + + private void bnTestDatabaseActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnTestDatabaseActionPerformed + lbTestDatabase.setIcon(null); + lbTestDbWarning.setText(""); + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + try { + CaseDbConnectionInfo info = new CaseDbConnectionInfo( + this.tbDbHostname.getText().trim(), + this.tbDbPort.getText().trim(), + this.tbDbUsername.getText().trim(), + new String(this.tbDbPassword.getPassword()), + DbType.POSTGRESQL); + + SleuthkitCase.tryConnect(info); + lbTestDatabase.setIcon(goodIcon); + lbTestDbWarning.setText(""); + } catch (TskCoreException ex) { + lbTestDatabase.setIcon(badIcon); + lbTestDbWarning.setText(ex.getMessage()); + } finally { + setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + }//GEN-LAST:event_bnTestDatabaseActionPerformed + + private void bnTestMessageServiceActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnTestMessageServiceActionPerformed + lbTestMessageService.setIcon(null); + lbTestMessageWarning.setText(""); + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + + int port; + try { + port = Integer.parseInt(this.tbMsgPort.getText().trim()); + } catch (NumberFormatException ex) { + lbTestMessageService.setIcon(badIcon); + lbTestMessageWarning.setText(NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.InvalidPortNumber")); + return; + } + + MessageServiceConnectionInfo info = new MessageServiceConnectionInfo( + this.tbMsgHostname.getText().trim(), + port, + this.tbMsgUsername.getText().trim(), + new String(this.tbMsgPassword.getPassword())); + try { + info.tryConnect(); + lbTestMessageService.setIcon(goodIcon); + lbTestMessageWarning.setText(""); + } catch (MessageServiceException ex) { + lbTestMessageService.setIcon(badIcon); + lbTestMessageWarning.setText(ex.getMessage()); + } finally { + setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + }//GEN-LAST:event_bnTestMessageServiceActionPerformed + + private void bnTestSolrActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnTestSolrActionPerformed + lbTestSolr.setIcon(null); + lbTestSolrWarning.setText(""); + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + + KeywordSearchService kwsService = Lookup.getDefault().lookup(KeywordSearchService.class); + try { + if (kwsService != null) { + int port = Integer.parseInt(tbSolrPort.getText().trim()); + kwsService.tryConnect(tbSolrHostname.getText().trim(), port); + lbTestSolr.setIcon(goodIcon); + lbTestSolrWarning.setText(""); + } else { + lbTestSolr.setIcon(badIcon); + lbTestSolrWarning.setText(NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.KeywordSearchNull")); + } + } catch (NumberFormatException ex) { + lbTestSolr.setIcon(badIcon); + lbTestSolrWarning.setText(NbBundle.getMessage(MultiUserSettingsPanel.class, "MultiUserSettingsPanel.InvalidPortNumber")); + } catch (KeywordSearchServiceException ex) { + lbTestSolr.setIcon(badIcon); + lbTestSolrWarning.setText(ex.getMessage()); + } finally { + setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + }//GEN-LAST:event_bnTestSolrActionPerformed + + void load() { + lbTestDatabase.setIcon(null); + lbTestSolr.setIcon(null); + lbTestMessageService.setIcon(null); + lbTestDbWarning.setText(""); + lbTestSolrWarning.setText(""); + lbTestMessageWarning.setText(""); + + try { + CaseDbConnectionInfo dbInfo = UserPreferences.getDatabaseConnectionInfo(); + tbDbHostname.setText(dbInfo.getHost().trim()); + tbDbPort.setText(dbInfo.getPort().trim()); + tbDbUsername.setText(dbInfo.getUserName().trim()); + tbDbPassword.setText(dbInfo.getPassword()); + } catch (UserPreferencesException ex) { + logger.log(Level.SEVERE, "Error accessing case database connection info", ex); //NON-NLS + } + + try { + MessageServiceConnectionInfo msgServiceInfo = UserPreferences.getMessageServiceConnectionInfo(); + tbMsgHostname.setText(msgServiceInfo.getHost().trim()); + tbMsgPort.setText(Integer.toString(msgServiceInfo.getPort())); + tbMsgUsername.setText(msgServiceInfo.getUserName().trim()); + tbMsgPassword.setText(msgServiceInfo.getPassword()); + } catch (UserPreferencesException ex) { + logger.log(Level.SEVERE, "Error accessing case database connection info", ex); //NON-NLS + } + + String indexingServerHost = UserPreferences.getIndexingServerHost().trim(); + if (!indexingServerHost.isEmpty()) { + tbSolrHostname.setText(indexingServerHost); + } + String indexingServerPort = UserPreferences.getIndexingServerPort().trim(); + if (portNumberIsValid(indexingServerPort)) { + tbSolrPort.setText(indexingServerPort); + } + + lbTestDatabase.setIcon(null); + lbTestSolr.setIcon(null); + lbTestMessageService.setIcon(null); + + bnTestDatabase.setEnabled(false); + bnTestSolr.setEnabled(false); + bnTestMessageService.setEnabled(false); + + cbEnableMultiUser.setSelected(UserPreferences.getIsMultiUserModeEnabled()); + this.valid(); // trigger validation to enable buttons based on current settings + } + + /** + * Tests whether or not values have been entered in all of the database + * settings text fields. + * + * @return True or false. + */ + private boolean databaseFieldsArePopulated() { + return !tbDbHostname.getText().trim().isEmpty() + && !tbDbPort.getText().trim().isEmpty() + && !tbDbUsername.getText().trim().isEmpty() + && tbDbPassword.getPassword().length != 0; + } + + /** + * Tests whether or not values have been entered in all of the Solr settings + * text fields. + * + * @return True or false. + */ + private boolean solrFieldsArePopulated() { + return !tbSolrHostname.getText().trim().isEmpty() + && !tbSolrPort.getText().trim().isEmpty(); + } + + /** + * Tests whether or not values have been entered in all of the message + * service settings text fields. + * + * @return True or false. + */ + private boolean messageServiceFieldsArePopulated() { + return !tbMsgHostname.getText().trim().isEmpty() + && !tbMsgPort.getText().trim().isEmpty() + && !tbMsgUsername.getText().trim().isEmpty() + && tbMsgPassword.getPassword().length != 0; + } + + void store() { + DbType dbType = DbType.SQLITE; + + if (cbEnableMultiUser.isSelected()) { + dbType = DbType.POSTGRESQL; + } + + UserPreferences.setIsMultiUserModeEnabled(cbEnableMultiUser.isSelected()); + + CaseDbConnectionInfo info = new CaseDbConnectionInfo( + tbDbHostname.getText().trim(), + tbDbPort.getText().trim(), + tbDbUsername.getText().trim(), + new String(tbDbPassword.getPassword()), + dbType); + + try { + UserPreferences.setDatabaseConnectionInfo(info); + } catch (UserPreferencesException ex) { + logger.log(Level.SEVERE, "Error accessing case database connection info", ex); //NON-NLS + } + + int port = 0; + try { + port = Integer.parseInt(this.tbMsgPort.getText().trim()); + } catch (NumberFormatException ex) { + logger.log(Level.SEVERE, "Bad port setting", ex); + } + + MessageServiceConnectionInfo msgServiceInfo = new MessageServiceConnectionInfo( + tbMsgHostname.getText().trim(), + port, + tbMsgUsername.getText().trim(), + new String(tbMsgPassword.getPassword())); + + try { + UserPreferences.setMessageServiceConnectionInfo(msgServiceInfo); + } catch (UserPreferencesException ex) { + logger.log(Level.SEVERE, "Error accessing messaging service connection info", ex); //NON-NLS + } + + UserPreferences.setIndexingServerHost(tbSolrHostname.getText().trim()); + UserPreferences.setIndexingServerPort(Integer.parseInt(tbSolrPort.getText().trim())); + + } + + /** + * Validates that the form is filled out correctly for our usage. + * + * @return true if it's okay, false otherwise. + */ + boolean valid() { + tbOops.setText(""); + + if (cbEnableMultiUser.isSelected()) { + return checkFieldsAndEnableButtons() + && databaseSettingsAreValid() + && indexingServerSettingsAreValid() + && messageServiceSettingsAreValid(); + } else { + return true; + } + } + + /** + * Tests whether or not all of the settings components are populated and + * sets the test buttons appropriately. + * + * @return True or false. + */ + boolean checkFieldsAndEnableButtons() { + boolean result = true; + + boolean dbPopulated = databaseFieldsArePopulated(); + boolean solrPopulated = solrFieldsArePopulated(); + boolean messageServicePopulated = messageServiceFieldsArePopulated(); + + // PostgreSQL Database + bnTestDatabase.setEnabled(dbPopulated); + + // Solr Indexing + bnTestSolr.setEnabled(solrPopulated); + + // ActiveMQ Messaging + bnTestMessageService.setEnabled(messageServicePopulated); + + if (!dbPopulated || !solrPopulated || !messageServicePopulated) { + // We don't even have everything filled out + result = false; + tbOops.setText(INCOMPLETE_SETTINGS_MSG); + } + return result; + } + + /** + * Tests whether or not the database settings are valid. + * + * @return True or false. + */ + boolean databaseSettingsAreValid() { + if (portNumberIsValid(tbDbPort.getText().trim())) { + return true; + } else { + tbOops.setText(INVALID_DB_PORT_MSG); + return false; + } + } + + /** + * Tests whether or not the message service settings are valid. + * + * @return True or false. + */ + boolean messageServiceSettingsAreValid() { + if (!portNumberIsValid(tbMsgPort.getText().trim())) { + tbOops.setText(INVALID_MESSAGE_SERVICE_PORT_MSG); + return false; + } + + return true; + } + + /** + * Tests whether or not the indexing server settings are valid. + * + * @return True or false. + */ + boolean indexingServerSettingsAreValid() { + if (!portNumberIsValid(tbSolrPort.getText().trim())) { + tbOops.setText(INVALID_INDEXING_SERVER_PORT_MSG); + return false; + } + + return true; + } + + /** + * Determines whether or not a port number is within the range of valid port + * numbers. + * + * @param portNumber The port number as a string. + * + * @return True or false. + */ + private static boolean portNumberIsValid(String portNumber) { + try { + int value = Integer.parseInt(portNumber); + if (value < 0 || value > 65535) { // invalid port numbers + return false; + } + } catch (NumberFormatException detailsNotImportant) { + return false; + } + return true; + } + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton bnTestDatabase; + private javax.swing.JButton bnTestMessageService; + private javax.swing.JButton bnTestSolr; + private javax.swing.JCheckBox cbEnableMultiUser; + private javax.swing.JLabel lbDatabaseSettings; + private javax.swing.JLabel lbMessageServiceSettings; + private javax.swing.JLabel lbSolrSettings; + private javax.swing.JLabel lbTestDatabase; + private javax.swing.JLabel lbTestDbWarning; + private javax.swing.JLabel lbTestMessageService; + private javax.swing.JLabel lbTestMessageWarning; + private javax.swing.JLabel lbTestSolr; + private javax.swing.JLabel lbTestSolrWarning; + private javax.swing.JPanel pnDatabaseSettings; + private javax.swing.JPanel pnMessagingSettings; + private javax.swing.JPanel pnOverallPanel; + private javax.swing.JPanel pnSolrSettings; + private javax.swing.JTextField tbDbHostname; + private javax.swing.JPasswordField tbDbPassword; + private javax.swing.JTextField tbDbPort; + private javax.swing.JTextField tbDbUsername; + private javax.swing.JTextField tbMsgHostname; + private javax.swing.JPasswordField tbMsgPassword; + private javax.swing.JTextField tbMsgPort; + private javax.swing.JTextField tbMsgUsername; + private javax.swing.JTextField tbOops; + private javax.swing.JTextField tbSolrHostname; + private javax.swing.JTextField tbSolrPort; + // End of variables declaration//GEN-END:variables + + /** + * Used to listen for changes in text boxes. It lets the panel know things + * have been updated and that validation needs to happen. + */ + class TextBoxChangedListener implements DocumentListener { + + @Override + public void changedUpdate(DocumentEvent e) { + Object statusIcon = e.getDocument().getProperty("statusIcon"); + if (statusIcon != null) { + ((javax.swing.JLabel) statusIcon).setIcon(null); + } + controller.changed(); + } + + @Override + public void insertUpdate(DocumentEvent e) { + Object statusIcon = e.getDocument().getProperty("statusIcon"); + if (statusIcon != null) { + ((javax.swing.JLabel) statusIcon).setIcon(null); + } + controller.changed(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + Object statusIcon = e.getDocument().getProperty("statusIcon"); + if (statusIcon != null) { + ((javax.swing.JLabel) statusIcon).setIcon(null); + } + controller.changed(); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/MultiUserSettingsPanelController.java b/Core/src/org/sleuthkit/autopsy/corecomponents/MultiUserSettingsPanelController.java new file mode 100644 index 0000000000..80b3f9bb58 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/MultiUserSettingsPanelController.java @@ -0,0 +1,130 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-2014 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.corecomponents; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import javax.swing.JComponent; +import org.netbeans.spi.options.OptionsPanelController; +import org.openide.util.HelpCtx; +import org.openide.util.Lookup; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import java.util.logging.Level; +import org.sleuthkit.autopsy.coreutils.Logger; + +@OptionsPanelController.TopLevelRegistration(categoryName = "#OptionsCategory_Name_Multi_User_Settings", + iconBase = "org/sleuthkit/autopsy/images/User-Group-icon-green32.png", + position = 2, + keywords = "#OptionsCategory_Keywords_Multi_User_Options", + keywordsCategory = "Multi-user") +public final class MultiUserSettingsPanelController extends OptionsPanelController { + + private MultiUserSettingsPanel panel; + private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); + private boolean changed; + private static final Logger logger = Logger.getLogger(MultiUserSettingsPanelController.class.getName()); + + @Override + public void update() { + getPanel().load(); + changed = false; + } + + @Override + public void applyChanges() { + getPanel().store(); + changed = false; + } + + @Override + public void cancel() { + } + + @Override + public boolean isValid() { + return getPanel().valid(); + } + + @Override + public boolean isChanged() { + return changed; + } + + @Override + public HelpCtx getHelpCtx() { + return null; + } + + @Override + public JComponent getComponent(Lookup masterLookup) { + return getPanel(); + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener l) { + if (pcs.getPropertyChangeListeners().length == 0) { + pcs.addPropertyChangeListener(l); + } + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener l) { + /** + * Note the NetBeans Framework does not appear to call this at all. We + * are using NetBeans 7.3.1 Build 201306052037. Perhaps in a future + * version of the Framework this will be resolved, but for now, simply + * don't unregister anything and add one time only in the + * addPropertyChangeListener() method above. + */ + } + + private MultiUserSettingsPanel getPanel() { + if (panel == null) { + panel = new MultiUserSettingsPanel(this); + } + return panel; + } + + void changed() { + if (!changed) { + changed = true; + + try { + pcs.firePropertyChange(OptionsPanelController.PROP_CHANGED, false, true); + } catch (Exception e) { + logger.log(Level.SEVERE, "GeneralOptionsPanelController listener threw exception", e); //NON-NLS + MessageNotifyUtil.Notify.show( + NbBundle.getMessage(this.getClass(), "GeneralOptionsPanelController.moduleErr"), + NbBundle.getMessage(this.getClass(), "GeneralOptionsPanelController.moduleErr.msg"), + MessageNotifyUtil.MessageType.ERROR); + } + } + + try { + pcs.firePropertyChange(OptionsPanelController.PROP_VALID, null, null); + } catch (Exception e) { + logger.log(Level.SEVERE, "GeneralOptionsPanelController listener threw exception", e); //NON-NLS + MessageNotifyUtil.Notify.show( + NbBundle.getMessage(this.getClass(), "GeneralOptionsPanelController.moduleErr"), + NbBundle.getMessage(this.getClass(), "GeneralOptionsPanelController.moduleErr.msg"), + MessageNotifyUtil.MessageType.ERROR); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/OfflineHelpAction.java b/Core/src/org/sleuthkit/autopsy/corecomponents/OfflineHelpAction.java index 2ab921907f..e06d61d778 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/OfflineHelpAction.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/OfflineHelpAction.java @@ -33,7 +33,7 @@ import org.openide.awt.HtmlBrowser; import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; import java.util.logging.Level; -import java.util.logging.Logger; +import org.sleuthkit.autopsy.coreutils.Logger; /** * Implements a hyperlink to the Offline Documentation. diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/OnlineHelpAction.java b/Core/src/org/sleuthkit/autopsy/corecomponents/OnlineHelpAction.java index d9190fa51b..aa23919d22 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/OnlineHelpAction.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/OnlineHelpAction.java @@ -35,7 +35,7 @@ import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; import java.util.logging.Level; -import java.util.logging.Logger; +import org.sleuthkit.autopsy.coreutils.Logger; /** * Implements a hyperlink to the Online Documentation. diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/TextPrompt.java b/Core/src/org/sleuthkit/autopsy/corecomponents/TextPrompt.java new file mode 100644 index 0000000000..01e035bfdf --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/TextPrompt.java @@ -0,0 +1,206 @@ +package org.sleuthkit.autopsy.corecomponents; + +import java.awt.*; +import java.awt.event.*; +import javax.swing.*; +import javax.swing.border.*; +import javax.swing.event.*; +import javax.swing.text.*; + +/** + * The TextPrompt class will display a prompt over top of a text component when + * the Document of the text field is empty. The Show property is used to + * determine the visibility of the prompt. + * + * The Font and foreground Color of the prompt will default to those properties + * of the parent text component. You are free to change the properties after + * class construction. From: + * https://tips4java.wordpress.com/2009/11/29/text-prompt/ + */ +public final class TextPrompt extends JLabel + implements FocusListener, DocumentListener { + + public enum Show { + + ALWAYS, + FOCUS_GAINED, + FOCUS_LOST; + } + + private JTextComponent component; + private Document document; + + private Show show; + private boolean showPromptOnce; + private int focusLost; + + public TextPrompt(String text, JTextComponent component) { + this(text, component, Show.ALWAYS); + } + + public TextPrompt(String text, JTextComponent component, Show show) { + this.component = component; + setShow(show); + document = component.getDocument(); + + setText(text); + setFont(component.getFont()); + setForeground(component.getForeground()); + setBorder(new EmptyBorder(component.getInsets())); + setHorizontalAlignment(JLabel.LEADING); + + component.addFocusListener(this); + document.addDocumentListener(this); + + component.setLayout(new BorderLayout()); + component.add(this); + checkForPrompt(); + } + + /** + * Convenience method to change the alpha value of the current foreground + * Color to the specifice value. + * + * @param alpha value in the range of 0 - 1.0. + */ + public void changeAlpha(float alpha) { + changeAlpha((int) (alpha * 255)); + } + + /** + * Convenience method to change the alpha value of the current foreground + * Color to the specifice value. + * + * @param alpha value in the range of 0 - 255. + */ + public void changeAlpha(int alpha) { + alpha = alpha > 255 ? 255 : alpha < 0 ? 0 : alpha; + + Color foreground = getForeground(); + int red = foreground.getRed(); + int green = foreground.getGreen(); + int blue = foreground.getBlue(); + + Color withAlpha = new Color(red, green, blue, alpha); + super.setForeground(withAlpha); + } + + /** + * Convenience method to change the style of the current Font. The style + * values are found in the Font class. Common values might be: Font.BOLD, + * Font.ITALIC and Font.BOLD + Font.ITALIC. + * + * @param style value representing the the new style of the Font. + */ + public void changeStyle(int style) { + setFont(getFont().deriveFont(style)); + } + + /** + * Get the Show property + * + * @return the Show property. + */ + public Show getShow() { + return show; + } + + /** + * Set the prompt Show property to control when the promt is shown. Valid + * values are: + * + * Show.AWLAYS (default) - always show the prompt Show.Focus_GAINED - show + * the prompt when the component gains focus (and hide the prompt when focus + * is lost) Show.Focus_LOST - show the prompt when the component loses focus + * (and hide the prompt when focus is gained) + * + * @param show a valid Show enum + */ + public void setShow(Show show) { + this.show = show; + } + + /** + * Get the showPromptOnce property + * + * @return the showPromptOnce property. + */ + public boolean getShowPromptOnce() { + return showPromptOnce; + } + + /** + * Show the prompt once. Once the component has gained/lost focus once, the + * prompt will not be shown again. + * + * @param showPromptOnce when true the prompt will only be shown once, + * otherwise it will be shown repeatedly. + */ + public void setShowPromptOnce(boolean showPromptOnce) { + this.showPromptOnce = showPromptOnce; + } + + /** + * Check whether the prompt should be visible or not. The visibility will + * change on updates to the Document and on focus changes. + */ + private void checkForPrompt() { + // Text has been entered, remove the prompt + + if (document.getLength() > 0) { + setVisible(false); + return; + } + + // Prompt has already been shown once, remove it + if (showPromptOnce && focusLost > 0) { + setVisible(false); + return; + } + + // Check the Show property and component focus to determine if the + // prompt should be displayed. + if (component.hasFocus()) { + if (show == Show.ALWAYS + || show == Show.FOCUS_GAINED) { + setVisible(true); + } else { + setVisible(false); + } + } else { + if (show == Show.ALWAYS + || show == Show.FOCUS_LOST) { + setVisible(true); + } else { + setVisible(false); + } + } + } + +// Implement FocusListener + @Override + public void focusGained(FocusEvent e) { + checkForPrompt(); + } + + @Override + public void focusLost(FocusEvent e) { + focusLost++; + checkForPrompt(); + } + +// Implement DocumentListener + @Override + public void insertUpdate(DocumentEvent e) { + checkForPrompt(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + checkForPrompt(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + } +} diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/checkbox32.png b/Core/src/org/sleuthkit/autopsy/corecomponents/checkbox32.png new file mode 100644 index 0000000000..e7a371fcbf Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/corecomponents/checkbox32.png differ diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/AutopsyExceptionHandler.java b/Core/src/org/sleuthkit/autopsy/coreutils/AutopsyExceptionHandler.java index 405cf3705d..3b7c333bc3 100644 --- a/Core/src/org/sleuthkit/autopsy/coreutils/AutopsyExceptionHandler.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/AutopsyExceptionHandler.java @@ -18,14 +18,12 @@ */ package org.sleuthkit.autopsy.coreutils; -import java.awt.Component; import java.util.logging.Filter; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.SimpleFormatter; import javax.swing.JOptionPane; -import javax.swing.SwingUtilities; import org.openide.util.lookup.ServiceProvider; import org.netbeans.core.NbErrorManager; @@ -62,32 +60,12 @@ public class AutopsyExceptionHandler extends Handler { public void publish(LogRecord record) { if (isLoggable(record)) { + final String title = getTitleForLevelValue(record.getLevel().intValue()); + final String message = formatExplanation(record); if (record.getMessage() != null) { // Throwable was anticipated, caught and logged. Display log message and throwable message. - - final int levelValue = record.getLevel().intValue(); - - final Component parentComponent = null; // Use default window frame. - final String message = formatExplanation(record); - final String title = getTitleForLevelValue(levelValue); - final int messageType = getMessageTypeForLevelValue(levelValue); - - // publish() was probably not called from the EDT, so run the message box there instead of here. - //only show the dialog in dev builds - if (buildType == Version.Type.DEVELOPMENT) { - SwingUtilities.invokeLater(new Runnable() { - - @Override - public void run() { - JOptionPane.showMessageDialog( - parentComponent, - message, - title, - messageType); - } - }); - } + MessageNotifyUtil.Notify.error(title, message); logger.log(Level.SEVERE, "Unexpected error: " + title + ", " + message); //NON-NLS } else { // Throwable (unanticipated) error. Use built-in exception handler to offer details, stacktrace. diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/ExecUtil.java b/Core/src/org/sleuthkit/autopsy/coreutils/ExecUtil.java index e63dd38fb8..b0a04d0d56 100644 --- a/Core/src/org/sleuthkit/autopsy/coreutils/ExecUtil.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/ExecUtil.java @@ -28,6 +28,7 @@ import java.util.Date; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.logging.Level; +import org.sleuthkit.autopsy.core.UserPreferences; /** * Executes a command line using an operating system process with a configurable @@ -41,7 +42,10 @@ public final class ExecUtil { /** * The execute() methods do a wait() with a timeout on the executing process * and query a process terminator each time the timeout expires to determine - * whether or not to kill the process. + * whether or not to kill the process. See + * DataSourceIngestModuleProcessTerminator and + * FileIngestModuleProcessTerminator as examples of ProcessTerminator + * implementations. */ public interface ProcessTerminator { @@ -74,6 +78,24 @@ public final class ExecUtil { this.startTimeInSeconds = (new Date().getTime()) / 1000; } + /** + * Creates a process terminator that can be used to kill a process after + * it has run for a given period of time. Maximum allowable run time is + * set via Autopsy Options panel. If the process termination + * functionality is disabled then the maximum allowable time is set to + * MAX_INT seconds. + */ + public TimedProcessTerminator() { + if (UserPreferences.getIsTimeOutEnabled() && UserPreferences.getProcessTimeOutHrs() > 0) { + // user specified time out + this.maxRunTimeInSeconds = UserPreferences.getProcessTimeOutHrs() * 3600; + } else { + // never time out + this.maxRunTimeInSeconds = Long.MAX_VALUE; + } + this.startTimeInSeconds = (new Date().getTime()) / 1000; + } + /** * @inheritDoc */ diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/FileUtil.java b/Core/src/org/sleuthkit/autopsy/coreutils/FileUtil.java index 5540fd8ef4..71494eae68 100644 --- a/Core/src/org/sleuthkit/autopsy/coreutils/FileUtil.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/FileUtil.java @@ -163,8 +163,8 @@ public class FileUtil { * @return escaped string */ public static String escapeFileName(String fileName) { - //for now escaping / (not valid in file name, at least on Windows) - //with underscores. Windows/Java seem to ignore \\/ and \\\\/ escapings - return fileName.replaceAll("/", "_"); + //for now escaping /:"*?<>| (not valid in file name, at least on Windows) + //with underscores. We are only keeping \ as it could be part of the path. + return fileName.replaceAll("[/:\"*?<>|]+", "_"); } } diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/History.java b/Core/src/org/sleuthkit/autopsy/coreutils/History.java index 50abf7e5cf..409cfe0006 100644 --- a/Core/src/org/sleuthkit/autopsy/coreutils/History.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/History.java @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.coreutils; +import java.util.Deque; import java.util.Objects; import javafx.beans.property.Property; import javafx.beans.property.ReadOnlyBooleanProperty; @@ -143,7 +144,6 @@ public class History { * @throws IllegalArgumentException if newState == null */ synchronized public void advance(T newState) throws IllegalArgumentException { - if (newState != null && Objects.equals(currentState.get(), newState) == false) { if (currentState.get() != null) { historyStack.push(currentState.get()); diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java b/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java index 26fffc6319..c9e337a29c 100755 --- a/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java @@ -56,7 +56,6 @@ import org.sleuthkit.datamodel.ReadContentInputStream; import org.sleuthkit.datamodel.TskCoreException; /** - * * Utilities for working with Images and creating thumbnails. Reuses thumbnails * by storing them in the case's cache directory. */ @@ -140,9 +139,6 @@ public class ImageUtils { = Executors.newSingleThreadExecutor(new BasicThreadFactory.Builder() .namingPattern("icon saver-%d").build()); - private ImageUtils() { - } - public static List getSupportedImageExtensions() { return Collections.unmodifiableList(SUPPORTED_IMAGE_EXTENSIONS); } diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/Logger.java b/Core/src/org/sleuthkit/autopsy/coreutils/Logger.java index e0ef048b13..dd2508a15d 100644 --- a/Core/src/org/sleuthkit/autopsy/coreutils/Logger.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/Logger.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2012-2014 Basis Technology Corp. + * Copyright 2012-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,17 +18,20 @@ */ package org.sleuthkit.autopsy.coreutils; -import java.io.File; import java.io.IOException; +import java.nio.file.Paths; import java.util.logging.FileHandler; import java.util.logging.Formatter; import java.util.logging.Handler; import java.sql.Timestamp; import java.util.Date; +import java.util.HashMap; +import java.util.Map; import java.util.logging.LogRecord; /** * Autopsy specialization of the Java Logger class with custom file handlers. + * Note that the custom loggers are not obtained from the global log manager. */ public final class Logger extends java.util.logging.Logger { @@ -37,66 +40,81 @@ public final class Logger extends java.util.logging.Logger { private static final int LOG_FILE_COUNT = 10; private static final String LOG_WITHOUT_STACK_TRACES = "autopsy.log"; //NON-NLS private static final String LOG_WITH_STACK_TRACES = "autopsy_traces.log"; //NON-NLS - private static final Handler console = new java.util.logging.ConsoleHandler(); - private static final Object fileHandlerLock = new Object(); - private static FileHandler userFriendlyLogFile = createFileHandler(PlatformUtil.getLogDirectory(), LOG_WITHOUT_STACK_TRACES); - private static FileHandler developersLogFile = createFileHandler(PlatformUtil.getLogDirectory(), LOG_WITH_STACK_TRACES); + private static final Map namesToLoggers = new HashMap<>(); + private static final Handler consoleHandler = new java.util.logging.ConsoleHandler(); + private static FileHandler userFriendlyHandler = createFileHandlerWithoutTraces(PlatformUtil.getLogDirectory()); + private static FileHandler developerFriendlyHandler = createFileHandlerWithTraces(PlatformUtil.getLogDirectory()); - private static FileHandler createFileHandler(String logDirectory, String fileName) { + /** + * Creates a custom file handler with a custom message formatter that does + * not include stack traces. + * + * @param logDirectory The directory where the log files should reside. + * + * @return A custom file handler. + */ + private static FileHandler createFileHandlerWithoutTraces(String logDirectory) { + String logFilePath = Paths.get(logDirectory, LOG_WITHOUT_STACK_TRACES).toString(); try { - FileHandler f = new FileHandler(logDirectory + File.separator + fileName, LOG_SIZE, LOG_FILE_COUNT); - f.setEncoding(LOG_ENCODING); - switch (fileName) { - case LOG_WITHOUT_STACK_TRACES: - f.setFormatter(new Formatter() { - @Override - public String format(LogRecord record) { - synchronized (fileHandlerLock) { - return (new Date(record.getMillis())).toString() + " " - + record.getSourceClassName() + " " - + record.getSourceMethodName() + "\n" - + record.getLevel() + ": " - + this.formatMessage(record) + "\n"; - } - } - }); - break; - case LOG_WITH_STACK_TRACES: - f.setFormatter(new Formatter() { - @Override - public String format(LogRecord record) { - synchronized (fileHandlerLock) { - if (record.getThrown() != null) { + FileHandler fileHandler = new FileHandler(logFilePath, LOG_SIZE, LOG_FILE_COUNT); + fileHandler.setEncoding(LOG_ENCODING); + fileHandler.setFormatter(new Formatter() { + @Override + public String format(LogRecord record) { + return (new Date(record.getMillis())).toString() + " " + + record.getSourceClassName() + " " + + record.getSourceMethodName() + "\n" + + record.getLevel() + ": " + + this.formatMessage(record) + "\n"; + } + }); + return fileHandler; + } catch (IOException ex) { + throw new RuntimeException(String.format("Error initializing file handler for %s", logFilePath), ex); //NON-NLS + } + } - StackTraceElement ele[] = record.getThrown().getStackTrace(); - String StackTrace = ""; - for (StackTraceElement ele1 : ele) { - StackTrace += "\t" + ele1.toString() + "\n"; - } - - return (new Timestamp(record.getMillis())).toString() + " " - + record.getSourceClassName() + " " - + record.getSourceMethodName() + "\n" - + record.getLevel() + ": " - + this.formatMessage(record) + "\n" - + record.getThrown().toString() + ":\n" - + StackTrace - + "\n"; - } else { - return (new Timestamp(record.getMillis())).toString() + " " - + record.getSourceClassName() + " " - + record.getSourceMethodName() + "\n" - + record.getLevel() + ": " - + this.formatMessage(record) + "\n"; - } - } + /** + * Creates a custom file handler with a custom message formatter that + * incldues stack traces. + * + * @param logDirectory The directory where the log files should reside. + * + * @return A custom file handler. + */ + private static FileHandler createFileHandlerWithTraces(String logDirectory) { + String logFilePath = Paths.get(logDirectory, LOG_WITH_STACK_TRACES).toString(); + try { + FileHandler fileHandler = new FileHandler(logFilePath, LOG_SIZE, LOG_FILE_COUNT); + fileHandler.setEncoding(LOG_ENCODING); + fileHandler.setFormatter(new Formatter() { + @Override + public String format(LogRecord record) { + if (record.getThrown() != null) { + String stackTrace = ""; //NON-NLS + for (StackTraceElement traceElem : record.getThrown().getStackTrace()) { + stackTrace += "\t" + traceElem.toString() + "\n"; //NON-NLS } - }); - break; - } - return f; - } catch (IOException e) { - throw new RuntimeException("Error initializing " + fileName + " file handler", e); //NON-NLS + return (new Timestamp(record.getMillis())).toString() + " " //NON-NLS + + record.getSourceClassName() + " " //NON-NLS + + record.getSourceMethodName() + "\n" //NON-NLS + + record.getLevel() + ": " //NON-NLS + + this.formatMessage(record) + "\n" //NON-NLS + + record.getThrown().toString() + ":\n" //NON-NLS + + stackTrace + + "\n"; //NON-NLS + } else { + return (new Timestamp(record.getMillis())).toString() + " " //NON-NLS + + record.getSourceClassName() + " " //NON-NLS + + record.getSourceMethodName() + "\n" //NON-NLS + + record.getLevel() + ": " //NON-NLS + + this.formatMessage(record) + "\n"; //NON-NLS + } + } + }); + return fileHandler; + } catch (IOException ex) { + throw new RuntimeException(String.format("Error initializing file handler for %s", logFilePath), ex); //NON-NLS } } @@ -105,63 +123,88 @@ public final class Logger extends java.util.logging.Logger { * * @param directoryPath The path to the desired log directory as a string. */ - public static void setLogDirectory(String directoryPath) { - if (null != directoryPath && !directoryPath.isEmpty()) { - File directory = new File(directoryPath); - if (directory.exists() && directory.canWrite()) { - synchronized (fileHandlerLock) { - userFriendlyLogFile.close(); - userFriendlyLogFile = createFileHandler(directoryPath, LOG_WITHOUT_STACK_TRACES); - developersLogFile.close(); - developersLogFile = createFileHandler(directoryPath, LOG_WITH_STACK_TRACES); - } - } + synchronized public static void setLogDirectory(String directoryPath) { + /* + * Create file handlers for the new directory and swap them into all of + * the existing loggers using thread-safe Logger methods. The new + * handlers are added before the old handlers so that no messages will + * be lost, but this makes it possible for log messages to be written + * via the old handlers if logging calls are interleaved with the + * add/remove handler calls (currently, the base class handlers + * collection is a CopyOnWriteArrayList). + */ + FileHandler newUserFriendlyHandler = createFileHandlerWithoutTraces(directoryPath); + FileHandler newDeveloperFriendlyHandler = createFileHandlerWithTraces(directoryPath); + for (Logger logger : namesToLoggers.values()) { + logger.addHandler(newUserFriendlyHandler); + logger.addHandler(newDeveloperFriendlyHandler); + logger.removeHandler(userFriendlyHandler); + logger.removeHandler(userFriendlyHandler); } + + /* + * Close the old file handlers and save references to the new handlers + * so they can be added to any new loggers. This swap is why this method + * and the two overloads of getLogger() are synchronized, serializing + * access to userFriendlyHandler and developerFriendlyHandler. + */ + userFriendlyHandler.close(); + userFriendlyHandler = newUserFriendlyHandler; + developerFriendlyHandler.close(); + developerFriendlyHandler = newDeveloperFriendlyHandler; } /** - * Factory method to retrieve a org.sleuthkit.autopsy.coreutils.Logger - * instance derived from java.util.logging.Logger. Hides the base class - * factory method. + * Finds or creates a customized logger. Hides the base class factory + * method. * - * @param name A name for the logger. This should be a dot-separated name - * and should normally be based on the package name or class - * name. + * @param name A name for the logger. This should normally be a + * dot-separated name based on a package name or class name. * * @return org.sleuthkit.autopsy.coreutils.Logger instance */ - public static Logger getLogger(String name) { - return new Logger(name, null); + synchronized public static Logger getLogger(String name) { + return getLogger(name, null); } /** - * Factory method to retrieve a org.sleuthkit.autopsy.coreutils.Logger - * instance derived from java.util.logging.Logger. Hides the base class - * factory method. + * Finds or creates a customized logger. Hides the base class factory + * method. * - * @param name A name for the logger. This should be a - * dot-separated name and should normally be based - * on the package name or class name. - * @param resourceBundleName - name of ResourceBundle to be used for + * @param name A name for the logger. This should normally be + * a dot-separated name based on a package name or + * class name. + * @param resourceBundleName Name of ResourceBundle to be used for * localizing messages for this logger. May be - * null if none of the messages require - * localization. + * null. * * @return org.sleuthkit.autopsy.coreutils.Logger instance */ - public static Logger getLogger(String name, String resourceBundleName) { - return new Logger(name, resourceBundleName); + synchronized public static Logger getLogger(String name, String resourceBundleName) { + if (!namesToLoggers.containsKey(name)) { + Logger logger = new Logger(name, resourceBundleName); + logger.addHandler(userFriendlyHandler); + logger.addHandler(developerFriendlyHandler); + namesToLoggers.put(name, logger); + } + return namesToLoggers.get(name); } + /** + * Constructs a customized logger. + * + * @param name A name for the logger. This should normally be + * a dot-separated name based on a package name or + * class name. + * @param resourceBundleName Name of ResourceBundle to be used for + * localizing messages for this logger. May be + * null. + */ private Logger(String name, String resourceBundleName) { super(name, resourceBundleName); + super.setUseParentHandlers(false); if (Version.getBuildType() == Version.Type.DEVELOPMENT) { - super.addHandler(console); - } - synchronized (fileHandlerLock) { - super.setUseParentHandlers(false); - super.addHandler(userFriendlyLogFile); - super.addHandler(developersLogFile); + super.addHandler(consoleHandler); } } } diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/ModuleSettings.java b/Core/src/org/sleuthkit/autopsy/coreutils/ModuleSettings.java index 2aa0990993..7f874395e3 100644 --- a/Core/src/org/sleuthkit/autopsy/coreutils/ModuleSettings.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/ModuleSettings.java @@ -39,6 +39,7 @@ public class ModuleSettings { private final static String moduleDirPath = PlatformUtil.getUserConfigDirectory(); public static final String DEFAULT_CONTEXT = "GeneralContext"; //NON-NLS public static final String MAIN_SETTINGS = "Case"; //NON-NLS + public static final String CURRENT_CASE_TYPE = "Current_Case_Type"; //NON-NLS /** * the constructor diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/NetworkUtils.java b/Core/src/org/sleuthkit/autopsy/coreutils/NetworkUtils.java new file mode 100644 index 0000000000..5564fa3f59 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/coreutils/NetworkUtils.java @@ -0,0 +1,44 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2012-2015 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.coreutils; + +import java.net.UnknownHostException; + +public class NetworkUtils { + + /** + * Set the host name variable. Sometimes the network can be finicky, so the + * answer returned by getHostName() could throw an exception or be null. + * Have it read the environment variable if getHostName() is unsuccessful. + */ + public static String getLocalHostName() { + String hostName = ""; + try { + hostName = java.net.InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException ex) { + // getLocalHost().getHostName() can fail in some situations. + // Use environment variable if so. + hostName = System.getenv("COMPUTERNAME"); + } + if (hostName == null || hostName.isEmpty()) { + hostName = System.getenv("COMPUTERNAME"); + } + return hostName; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/PathValidator.java b/Core/src/org/sleuthkit/autopsy/coreutils/PathValidator.java new file mode 100644 index 0000000000..41b5c19d62 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/coreutils/PathValidator.java @@ -0,0 +1,58 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-2014 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.coreutils; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.sleuthkit.autopsy.casemodule.Case; + +/** + * Validates absolute path (e.g. to a data source or case output folder) + * depending on case type. + */ +public final class PathValidator { + + private static final Pattern driveLetterPattern = Pattern.compile("^[Cc]:.*$"); + + public static boolean isValid(String path, Case.CaseType caseType) { + + if (caseType == Case.CaseType.MULTI_USER_CASE) { + // check that path is not on "C:" drive + if (pathOnCDrive(path)) { + return false; + } + } else { + // single user case - no validation needed + } + + return true; + } + + /** + * Checks whether a file path contains drive letter defined by pattern. + * + * @param filePath Input file absolute path + * + * @return true if path matches the pattern, false otherwise. + */ + private static boolean pathOnCDrive(String filePath) { + Matcher m = driveLetterPattern.matcher(filePath); + return m.find(); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/TimeStampUtils.java b/Core/src/org/sleuthkit/autopsy/coreutils/TimeStampUtils.java new file mode 100644 index 0000000000..05e8cb9a52 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/coreutils/TimeStampUtils.java @@ -0,0 +1,97 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.coreutils; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Class offers utility functions to identify and process time stamped folders. + */ +public final class TimeStampUtils { + + // Pattern to identify whether case name contains a generated time stamp. + // Sample case name with time stamp: Case 1_2015_02_02_12_10_31 for case "Case 1" + private static final Pattern timeStampPattern = Pattern.compile("\\d{4}_\\d{2}_\\d{2}_\\d{2}_\\d{2}_\\d{2}$"); + private static final int LENGTH_OF_DATE_TIME_STAMP = 20; // length of the above time stamp + private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy_MM_dd_HH_mm_ss"); + + /** + * Checks whether a string ends with a time stamp defined by pattern. + * + * @param inputString Input string + * + * @return true if string ends with a time stamp, false otherwise. + */ + public static boolean endsWithTimeStamp(String inputString) { + Matcher m = timeStampPattern.matcher(inputString); + return m.find(); + } + + /** + * Returns length of time stamp string. + * + * @return length of time stamp string. + */ + public static int getTimeStampLength() { + return LENGTH_OF_DATE_TIME_STAMP; + } + + /** + * Create a timestamp using the current time + * + * @return the timestamp as a String + */ + public static String createTimeStamp() { + return dateFormat.format(Calendar.getInstance().getTime()); + } + + /** + * Remove a timestamp if it exists + * + * @param input the String to remove the trailing timestamp from + * + * @return the String without timestamp + */ + public static String removeTimeStamp(String input) { + String result = input; + if (input != null && endsWithTimeStamp(input)) { + result = input.substring(0, input.length() - getTimeStampLength()); + } + return result; + } + + /** + * Return the timestamp portion of the name passed in + * + * @param input the name to check for a timestamp + * + * @return the timestamp only, or empty String if none + */ + public static String getTimeStampOnly(String input) { + String result = ""; + if (input != null && endsWithTimeStamp(input)) { + result = input.substring(input.length() - getTimeStampLength(), input.length()); + } + return result; + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/TimestampingFormatter.java b/Core/src/org/sleuthkit/autopsy/coreutils/TimestampingFormatter.java deleted file mode 100644 index bbde8529f8..0000000000 --- a/Core/src/org/sleuthkit/autopsy/coreutils/TimestampingFormatter.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2011 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.coreutils; - -import java.text.DateFormat; -import java.util.Date; -import java.util.Locale; -import java.util.logging.Formatter; -import java.util.logging.LogRecord; - -/* - * Formatter to wrap another formatter and prepend a timestampe to each - * formatted string Not currently used. - */ -class TimestampingFormatter extends Formatter { - - Formatter original; - DateFormat timestampFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, Locale.US); - String lineSeparator = System.getProperty("line.separator"); - - TimestampingFormatter(Formatter original) { - this.original = original; - } - - @Override - public String format(LogRecord record) { - long millis = record.getMillis(); - String timestamp = timestampFormat.format(new Date(millis)); - - return timestamp + lineSeparator + original.format(record); - } -} diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/UNCPathUtilities.java b/Core/src/org/sleuthkit/autopsy/coreutils/UNCPathUtilities.java new file mode 100644 index 0000000000..c919eab11b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/coreutils/UNCPathUtilities.java @@ -0,0 +1,308 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.coreutils; + +import java.io.File; +import java.io.IOException; +import java.net.InetAddress; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +public class UNCPathUtilities { + + private static Map drives; + private static final String MAPPED_DRIVES = "_mapped_drives.txt"; //NON-NLS + private static final String TEMP_FOLDER = "TEMP"; + private static final String DATA_TRIGGER = "----------"; //NON-NLS + private static final String OK_TXT = "OK"; //NON-NLS + private static final String COLON = ":"; //NON-NLS + private static final String UNC_PATH_START = "\\\\"; //NON-NLS + private static final String C_DRIVE = "C:"; //NON-NLS + private static final int DRIVE_LEN = 2; + private static final int STARTING_OFFSET = 0; + private static final int REPLACEMENT_SIZE = 2; + private static final int FIRST_ITEM = 0; + private final String nameString; + + /** + * Constructor + */ + public UNCPathUtilities() { + // get UUID for this instance + this.nameString = UUID.randomUUID().toString(); + drives = getMappedDrives(); + } + + /** + * This method converts a passed in path to UNC if it is not already UNC. + * The UNC path will end up in one of the following two forms: + * \\hostname\somefolder\otherfolder or \\IP_ADDRESS\somefolder\otherfolder + * + * This is accomplished by checking the mapped drives list the operating + * system maintains and substituting where required. If the drive of the + * path passed in does not exist in the cached mapped drives list, you can + * force a rescan of the mapped drives list with rescanDrives(), then call + * this method again. This would be of use if the end user added a mapped + * drive while your dialog was up, for example. + * + * @param inputPath a String of the path to convert + * + * @return returns a successfully converted inputPath or null if unable to + * find a matching drive and convert it to UNC + */ + synchronized public String mappedDriveToUNC(String inputPath) { + if (inputPath != null) { + // If it is a C:, do not attempt to convert. This is for the single-user case. + if (inputPath.toUpperCase().startsWith(C_DRIVE)) { + return null; + } + if (false == isUNC(inputPath)) { + String uncPath = null; + try { + String currentDrive = Paths.get(inputPath).getRoot().toString().substring(STARTING_OFFSET, REPLACEMENT_SIZE); + String uncMapping = drives.get(currentDrive); + if (uncMapping != null) { + uncPath = uncMapping + inputPath.substring(REPLACEMENT_SIZE, inputPath.length()); + } + } catch (Exception ex) { + // Didn't work. Skip it. + } + return uncPath; + } else { + return inputPath; + } + } else { + return null; + } + } + + /** + * This method converts a passed in path to UNC if it is not already UNC. + * The UNC path will end up in one of the following two forms: + * \\hostname\somefolder\otherfolder or \\IP_ADDRESS\somefolder\otherfolder + * + * This is accomplished by checking the mapped drives list the operating + * system maintains and substituting where required. If the drive of the + * path passed in does not exist in the cached mapped drives list, you can + * force a rescan of the mapped drives list with rescanDrives(), then call + * this method again. This would be of use if the end user added a mapped + * drive while your dialog was up, for example. + * + * @param inputPath the path to convert + * + * @return returns a successfully converted inputPath or null if unable to + * find a matching drive and convert it to UNC + */ + synchronized public Path mappedDriveToUNC(Path inputPath) { + if (inputPath != null) { + String uncPath = UNCPathUtilities.this.mappedDriveToUNC(inputPath.toString()); + if (uncPath == null) { + return null; + } else { + return Paths.get(uncPath); + } + } else { + return null; + } + } + + /** + * Tests if the drive in the passed in path is a mapped drive. + * + * @param inputPath the Path to test. + * + * @return true if the passed in drive is mapped, false otherwise + */ + synchronized public boolean isDriveMapped(Path inputPath) { + if (inputPath != null) { + return isDriveMapped(inputPath.toString()); + } else { + return false; + } + } + + /** + * Tests if the drive in the passed in path is a mapped drive. + * + * @param inputPath the Path to test. + * + * @return true if the passed in drive is mapped, false otherwise + */ + synchronized public boolean isDriveMapped(String inputPath) { + if (inputPath != null) { + String shortenedPath = inputPath.substring(STARTING_OFFSET, DRIVE_LEN); + for (String s : drives.keySet()) { + if (shortenedPath.equals(s)) { + return true; + } + } + } + return false; + } + + /** + * Takes a UNC path that may have an IP address in it and converts it to + * hostname, if it can resolve the hostname. Given + * \\10.11.12.13\some\folder, the result will be \\TEDS_COMPUTER\some\folder + * if the IP address 10.11.12.13 belongs to a machine with the hostname + * TEDS_COMPUTER and the local machine is able to resolve the hostname. + * + * @param inputPath the path to convert to a hostname UNC path + * + * @return the successfully converted path or null if unable to resolve + */ + synchronized public Path ipToHostName(Path inputPath) { + if (inputPath != null) { + return Paths.get(ipToHostName(inputPath.toString())); + } else { + return null; + } + } + + /** + * Takes a UNC path that may have an IP address in it and converts it to + * hostname, if it can resolve the hostname. Given + * \\10.11.12.13\some\folder, the result will be \\TEDS_COMPUTER\some\folder + * if the IP address 10.11.12.13 belongs to a machine with the hostname + * TEDS_COMPUTER and the local machine is able to resolve the hostname. + * + * @param inputPath a String of the path to convert to a hostname UNC path + * + * @return the successfully converted path or null if unable to resolve + */ + synchronized public String ipToHostName(String inputPath) { + if (inputPath != null) { + String result = null; + try { + if (isUNC(Paths.get(inputPath))) { + String potentialIP = Paths.get(inputPath.substring(REPLACEMENT_SIZE)).getName(FIRST_ITEM).toString(); + String hostname = InetAddress.getByName(potentialIP).getHostName(); + result = inputPath.replaceAll(potentialIP, hostname); + } + } catch (Exception ex) { + // Could not resolve hostname for IP address, return null result + } + return result; + } else { + return null; + } + } + + /** + * Test if a Path is UNC. It is considered UNC if it begins with \\ + * + * @param inputPath the path to check + * + * @return true if the passed in Path is UNC, false otherwise + */ + synchronized public static boolean isUNC(Path inputPath) { + if (inputPath != null) { + return isUNC(inputPath.toString()); + } else { + return false; + } + } + + /** + * Test if a String path is UNC. It is considered UNC if it begins with \\ + * + * @param inputPath the String of the path to check + * + * @return true if the passed in Path is UNC, false otherwise + */ + synchronized public static boolean isUNC(String inputPath) { + if (inputPath != null) { + return inputPath.startsWith(UNC_PATH_START); + } else { + return false; + } + } + + /** + * Updates the list of mapped drives this class contains. This list is used + * to resolve mappedDriveToUNC and isDriveMapped calls. This is useful to + * call if the user has potentially added mapped drives to their system + * after the module calling mappedDriveToUNC has already begun running. Note + * this uses system I/O, so call it with some care. + * + */ + synchronized public void rescanDrives() { + drives = getMappedDrives(); + } + + /** + * Populates the list of mapped drives this class contains. The list is used + * to resolve mappedDriveToUNC and isDriveMapped calls. Note this uses + * system I/O, so call it with some care. + * + * @return the hashmap + */ + synchronized private Map getMappedDrives() { + Map driveMap = new HashMap<>(); + File mappedDrive = Paths.get(System.getenv(TEMP_FOLDER), nameString + MAPPED_DRIVES).toFile(); + try { + Files.deleteIfExists(mappedDrive.toPath()); + ProcessBuilder builder = new ProcessBuilder("cmd", "/c", "net", "use"); //NON-NLS + builder.redirectOutput(mappedDrive); + builder.redirectError(mappedDrive); + Process p = builder.start(); // throws IOException + p.waitFor(10, TimeUnit.SECONDS); + try (Scanner scanner = new Scanner(mappedDrive)) { + // parse the data and place it in the hashmap + while (scanner.hasNext()) { + String entry1 = scanner.next(); + if (entry1.startsWith(DATA_TRIGGER)) { + continue; + } + String entry2 = scanner.next(); + if (entry2.startsWith(DATA_TRIGGER)) { + continue; + } + String entry3 = scanner.next(); + if (entry3.startsWith(DATA_TRIGGER)) { + continue; + } + scanner.nextLine(); + if (entry1.length() == DRIVE_LEN && !entry1.equals(OK_TXT) && entry1.endsWith(COLON)) { + driveMap.put(entry1, entry2); // if there was no leading status, populate drive + } else if (entry2.length() == DRIVE_LEN && entry2.endsWith(COLON)) { + driveMap.put(entry2, entry3); // if there was a leading status, populate drive + } + } + } + } catch (IOException | InterruptedException ex) { + // if we couldn't do it, no big deal + Logger.getLogger(UNCPathUtilities.class.getName()).log(Level.WARNING, "Unable to parse 'net use' output", ex); //NON-NLS + } finally { + try { + Files.deleteIfExists(mappedDrive.toPath()); + } catch (IOException ex) { + // if we couldn't do it, no big deal + } + } + return driveMap; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java index fad5cde64c..a1ef63eac1 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java @@ -18,12 +18,18 @@ */ package org.sleuthkit.autopsy.datamodel; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import org.openide.nodes.Children; import java.util.Map; import java.util.logging.Level; import org.apache.commons.lang3.StringUtils; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.autopsy.ingest.ModuleContentEvent; import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.TskCoreException; /** @@ -41,8 +47,53 @@ public abstract class AbstractAbstractFileNode extends A */ AbstractAbstractFileNode(T abstractFile) { super(abstractFile); + String name = abstractFile.getName(); + int dotIndex = name.lastIndexOf("."); + if (dotIndex > 0) { + String ext = name.substring(dotIndex).toLowerCase(); + + // If this is an archive file we will listen for ingest events + // that will notify us when new content has been identified. + for (String s : FileTypeExtensions.getArchiveExtensions()) { + if (ext.equals(s)) { + IngestManager.getInstance().addIngestModuleEventListener(pcl); + } + } + } + } + private final PropertyChangeListener pcl = (PropertyChangeEvent evt) -> { + String eventType = evt.getPropertyName(); + + // Is this a content changed event? + if (eventType.equals(IngestManager.IngestModuleEvent.CONTENT_CHANGED.toString())) { + if ((evt.getOldValue() instanceof ModuleContentEvent) == false) { + return; + } + ModuleContentEvent moduleContentEvent = (ModuleContentEvent) evt.getOldValue(); + if ((moduleContentEvent.getSource() instanceof Content) == false) { + return; + } + Content newContent = (Content) moduleContentEvent.getSource(); + + // Does the event indicate that content has been added to *this* file? + if (getContent().getId() == newContent.getId()) { + // If so, refresh our children. + try { + Children parentsChildren = getParentNode().getChildren(); + if (parentsChildren != null) { + ((ContentChildren) parentsChildren).refreshChildren(); + parentsChildren.getNodesCount(); + } + } catch (NullPointerException ex) { + // Skip + } + + } + } + }; + // Note: this order matters for the search result, changed it if the order of property headers on the "KeywordSearchNode"changed public static enum AbstractFilePropertyType { diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ContentChildren.java b/Core/src/org/sleuthkit/autopsy/datamodel/ContentChildren.java index b151c0192a..3f8dcfdb07 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/ContentChildren.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ContentChildren.java @@ -116,6 +116,16 @@ class ContentChildren extends AbstractContentChildren { @Override protected void removeNotify() { super.removeNotify(); - setKeys(new ArrayList()); + setKeys(new ArrayList<>()); + } + + /** + * Refresh the list of children due to a change in one (or more) of our + * children (e.g. archive files can change as new content is extracted from + * them). + */ + void refreshChildren() { + List children = getDisplayChildren(parent); + setKeys(children); } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DataSourcesNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/DataSourcesNode.java index 0c7cecd8d0..4d3331fac1 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DataSourcesNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DataSourcesNode.java @@ -59,7 +59,6 @@ public class DataSourcesNode extends DisplayableItemNode { /* * Custom Keys implementation that listens for new data sources being added. */ - // @@@ This can become private once DirectoryTree doesn't want to refresh the entire tree public static class DataSourcesNodeChildren extends AbstractContentChildren { private static final Logger logger = Logger.getLogger(DataSourcesNodeChildren.class.getName()); @@ -107,9 +106,6 @@ public class DataSourcesNode extends DisplayableItemNode { /** * Refresh all content keys This creates new nodes of keys have changed. */ - // I think this goes away once we get more listeners in place - // It was added as an interim stage - @Deprecated public void refreshContentKeys() { for (Content key : currentKeys) { refreshKey(key); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DeletedContent.java b/Core/src/org/sleuthkit/autopsy/datamodel/DeletedContent.java index 6d57c3e674..9d6e745c29 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DeletedContent.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DeletedContent.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-2014 Basis Technology Corp. + * Copyright 2013-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -183,16 +183,42 @@ public class DeletedContent implements AutopsyVisitableItem { @Override public void propertyChange(PropertyChangeEvent evt) { String eventType = evt.getPropertyName(); - - // new file was added if (eventType.equals(IngestManager.IngestModuleEvent.CONTENT_CHANGED.toString())) { - // @@@ COULD CHECK If the new file is deleted before notifying... - update(); + /** + * + // @@@ COULD CHECK If the new file is deleted + * before notifying... Checking for a current case is a + * stop gap measure + update(); until a different way of + * handling the closing of cases is worked out. + * Currently, remote events may be received for a case + * that is already closed. + */ + try { + Case.getCurrentCase(); + // new file was added + // @@@ COULD CHECK If the new file is deleted before notifying... + update(); + } catch (IllegalStateException notUsed) { + /** + * Case is closed, do nothing. + */ + } } else if (eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString()) - || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString())) { - update(); - } else if (eventType.equals(Case.Events.DATA_SOURCE_ADDED.toString())) { - update(); + || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString()) + || eventType.equals(Case.Events.DATA_SOURCE_ADDED.toString())) { + /** + * Checking for a current case is a stop gap measure + * until a different way of handling the closing of + * cases is worked out. Currently, remote events may be + * received for a case that is already closed. + */ + try { + Case.getCurrentCase(); + update(); + } catch (IllegalStateException notUsed) { + /** + * Case is closed, do nothing. + */ + } } else if (eventType.equals(Case.Events.CURRENT_CASE.toString())) { // case was closed. Remove listeners so that we don't get called with a stale case handle if (evt.getNewValue() == null) { @@ -210,6 +236,7 @@ public class DeletedContent implements AutopsyVisitableItem { } @Override + protected boolean createKeys(List list) { list.addAll(Arrays.asList(DeletedContent.DeletedContentFilter.values())); return true; @@ -222,8 +249,7 @@ public class DeletedContent implements AutopsyVisitableItem { public class DeletedContentNode extends DisplayableItemNode { - private DeletedContent.DeletedContentFilter filter; - private final Logger logger = Logger.getLogger(DeletedContentNode.class.getName()); + private final DeletedContent.DeletedContentFilter filter; // Use version that has observer for updates @Deprecated @@ -296,8 +322,8 @@ public class DeletedContent implements AutopsyVisitableItem { static class DeletedContentChildren extends ChildFactory.Detachable { - private SleuthkitCase skCase; - private DeletedContent.DeletedContentFilter filter; + private final SleuthkitCase skCase; + private final DeletedContent.DeletedContentFilter filter; private static final Logger logger = Logger.getLogger(DeletedContentChildren.class.getName()); private static final int MAX_OBJECTS = 10001; private final Observable notifier; @@ -339,7 +365,6 @@ public class DeletedContent implements AutopsyVisitableItem { List queryList = runFsQuery(); if (queryList.size() == MAX_OBJECTS) { queryList.remove(queryList.size() - 1); - // only show the dialog once - not each time we refresh if (maxFilesDialogShown == false) { maxFilesDialogShown = true; diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/EmailExtracted.java b/Core/src/org/sleuthkit/autopsy/datamodel/EmailExtracted.java index 6a02438ea0..a28992c978 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/EmailExtracted.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/EmailExtracted.java @@ -205,14 +205,46 @@ public class EmailExtracted implements AutopsyVisitableItem { @Override public void propertyChange(PropertyChangeEvent evt) { String eventType = evt.getPropertyName(); - if (eventType.equals(IngestManager.IngestModuleEvent.DATA_ADDED.toString())) { - if (((ModuleDataEvent) evt.getOldValue()).getArtifactType() == BlackboardArtifact.ARTIFACT_TYPE.TSK_EMAIL_MSG) { - emailResults.update(); + /** + * Checking for a current case is a stop gap measure until a + * different way of handling the closing of cases is worked + * out. Currently, remote events may be received for a case + * that is already closed. + */ + try { + Case.getCurrentCase(); + /** + * Even with the check above, it is still possible that + * the case will be closed in a different thread before + * this code executes. If that happens, it is possible + * for the event to have a null oldValue. + */ + ModuleDataEvent eventData = (ModuleDataEvent) evt.getOldValue(); + if (null != eventData && eventData.getArtifactType() == BlackboardArtifact.ARTIFACT_TYPE.TSK_EMAIL_MSG) { + emailResults.update(); + } + } catch (IllegalStateException notUsed) { + /** + * Case is closed, do nothing. + */ } } else if (eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString()) || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString())) { - emailResults.update(); + /** + * Checking for a current case is a stop gap measure until a + * different way of handling the closing of cases is worked + * out. Currently, remote events may be received for a case + * that is already closed. + */ + try { + Case.getCurrentCase(); + emailResults.update(); + } catch (IllegalStateException notUsed) { + /** + * Case is closed, do nothing. + */ + } } else if (eventType.equals(Case.Events.CURRENT_CASE.toString())) { // case was closed. Remove listeners so that we don't get called with a stale case handle if (evt.getNewValue() == null) { diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ExtractedContent.java b/Core/src/org/sleuthkit/autopsy/datamodel/ExtractedContent.java index 86aecf3d57..4fa27c8131 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/ExtractedContent.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ExtractedContent.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2014 Basis Technology Corp. + * Copyright 2011-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -127,7 +127,7 @@ public class ExtractedContent implements AutopsyVisitableItem { */ private class TypeFactory extends ChildFactory.Detachable { - private final ArrayList doNotShow; + private final ArrayList doNotShow = new ArrayList<>(); // maps the artifact type to its child node private final HashMap typeNodeList = new HashMap<>(); @@ -135,7 +135,6 @@ public class ExtractedContent implements AutopsyVisitableItem { super(); // these are shown in other parts of the UI tree - doNotShow = new ArrayList<>(); doNotShow.add(BlackboardArtifact.ARTIFACT_TYPE.TSK_GEN_INFO); doNotShow.add(BlackboardArtifact.ARTIFACT_TYPE.TSK_EMAIL_MSG); doNotShow.add(BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT); @@ -144,25 +143,50 @@ public class ExtractedContent implements AutopsyVisitableItem { doNotShow.add(BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_ARTIFACT_HIT); } - private final PropertyChangeListener pcl = new PropertyChangeListener() { - @Override - public void propertyChange(PropertyChangeEvent evt) { - String eventType = evt.getPropertyName(); - - if (eventType.equals(IngestManager.IngestModuleEvent.DATA_ADDED.toString())) { + private final PropertyChangeListener pcl = (PropertyChangeEvent evt) -> { + String eventType = evt.getPropertyName(); + if (eventType.equals(IngestManager.IngestModuleEvent.DATA_ADDED.toString())) { + /** + * This is a stop gap measure until a different way of handling + * the closing of cases is worked out. Currently, remote events + * may be received for a case that is already closed. + */ + try { + Case.getCurrentCase(); + /** + * Due to some unresolved issues with how cases are closed, + * it is possible for the event to have a null oldValue if + * the event is a remote event. + */ final ModuleDataEvent event = (ModuleDataEvent) evt.getOldValue(); - if (doNotShow.contains(event.getArtifactType()) == false) { + if (null != event && doNotShow.contains(event.getArtifactType()) == false) { refresh(true); } - } else if (eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString()) - || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString())) { + } catch (IllegalStateException notUsed) { + /** + * Case is closed, do nothing. + */ + } + } else if (eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString()) + || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString())) { + /** + * This is a stop gap measure until a different way of handling + * the closing of cases is worked out. Currently, remote events + * may be received for a case that is already closed. + */ + try { + Case.getCurrentCase(); refresh(true); - } else if (eventType.equals(Case.Events.CURRENT_CASE.toString())) { - // case was closed. Remove listeners so that we don't get called with a stale case handle - if (evt.getNewValue() == null) { - removeNotify(); - skCase = null; - } + } catch (IllegalStateException notUsed) { + /** + * Case is closed, do nothing. + */ + } + } else if (eventType.equals(Case.Events.CURRENT_CASE.toString())) { + // case was closed. Remove listeners so that we don't get called with a stale case handle + if (evt.getNewValue() == null) { + removeNotify(); + skCase = null; } } }; @@ -184,33 +208,30 @@ public class ExtractedContent implements AutopsyVisitableItem { @Override protected boolean createKeys(List list) { - if (skCase == null) { - return false; - } + if (skCase != null) { + try { + List inUse = skCase.getBlackboardArtifactTypesInUse(); + inUse.removeAll(doNotShow); + Collections.sort(inUse, + new Comparator() { + @Override + public int compare(BlackboardArtifact.ARTIFACT_TYPE a, BlackboardArtifact.ARTIFACT_TYPE b) { + return a.getDisplayName().compareTo(b.getDisplayName()); + } + }); + list.addAll(inUse); - try { - List inUse = skCase.getBlackboardArtifactTypesInUse(); - inUse.removeAll(doNotShow); - Collections.sort(inUse, - new Comparator() { - @Override - public int compare(BlackboardArtifact.ARTIFACT_TYPE a, BlackboardArtifact.ARTIFACT_TYPE b) { - return a.getDisplayName().compareTo(b.getDisplayName()); - } - }); - list.addAll(inUse); - - // the create node method will get called only for new types - // refresh the counts if we already created them from a previous update - for (BlackboardArtifact.ARTIFACT_TYPE art : inUse) { - TypeNode node = typeNodeList.get(art); - if (node != null) { - node.updateDisplayName(); + // the create node method will get called only for new types + // refresh the counts if we already created them from a previous update + for (BlackboardArtifact.ARTIFACT_TYPE art : inUse) { + TypeNode node = typeNodeList.get(art); + if (node != null) { + node.updateDisplayName(); + } } + } catch (TskCoreException ex) { + Logger.getLogger(TypeFactory.class.getName()).log(Level.SEVERE, "Error getting list of artifacts in use: " + ex.getLocalizedMessage()); //NON-NLS } - } catch (TskCoreException ex) { - Logger.getLogger(TypeFactory.class.getName()).log(Level.SEVERE, "Error getting list of artifacts in use: " + ex.getLocalizedMessage()); //NON-NLS - return false; } return true; } @@ -335,6 +356,8 @@ public class ExtractedContent implements AutopsyVisitableItem { return "mismatch-16.png"; //NON-NLS case TSK_OS_INFO: return "computer.png"; //NON-NLS + case TSK_FACE_DETECTED: + return "face.png"; //NON-NLS } return "artifact-icon.png"; //NON-NLS @@ -362,15 +385,46 @@ public class ExtractedContent implements AutopsyVisitableItem { @Override public void propertyChange(PropertyChangeEvent evt) { String eventType = evt.getPropertyName(); - if (eventType.equals(IngestManager.IngestModuleEvent.DATA_ADDED.toString())) { - final ModuleDataEvent event = (ModuleDataEvent) evt.getOldValue(); - if (event.getArtifactType() == type) { - refresh(true); + /** + * Checking for a current case is a stop gap measure until a + * different way of handling the closing of cases is worked + * out. Currently, remote events may be received for a case + * that is already closed. + */ + try { + Case.getCurrentCase(); + /** + * Even with the check above, it is still possible that + * the case will be closed in a different thread before + * this code executes. If that happens, it is possible + * for the event to have a null oldValue. + */ + final ModuleDataEvent event = (ModuleDataEvent) evt.getOldValue(); + if (null != event && event.getArtifactType() == type) { + refresh(true); + } + } catch (IllegalStateException notUsed) { + /** + * Case is closed, do nothing. + */ } } else if (eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString()) || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString())) { - refresh(true); + /** + * Checking for a current case is a stop gap measure until a + * different way of handling the closing of cases is worked + * out. Currently, remote events may be received for a case + * that is already closed. + */ + try { + Case.getCurrentCase(); + refresh(true); + } catch (IllegalStateException notUsed) { + /** + * Case is closed, do nothing. + */ + } } } }; @@ -389,15 +443,13 @@ public class ExtractedContent implements AutopsyVisitableItem { @Override protected boolean createKeys(List list) { - if (skCase == null) { - return false; - } - - try { - List arts = skCase.getBlackboardArtifacts(type.getTypeID()); - list.addAll(arts); - } catch (TskException ex) { - Logger.getLogger(ArtifactFactory.class.getName()).log(Level.SEVERE, "Couldn't get blackboard artifacts from database", ex); //NON-NLS + if (skCase != null) { + try { + List arts = skCase.getBlackboardArtifacts(type.getTypeID()); + list.addAll(arts); + } catch (TskException ex) { + Logger.getLogger(ArtifactFactory.class.getName()).log(Level.SEVERE, "Couldn't get blackboard artifacts from database", ex); //NON-NLS + } } return true; } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/FileSize.java b/Core/src/org/sleuthkit/autopsy/datamodel/FileSize.java index cbae247d55..817db770b7 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/FileSize.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/FileSize.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-2014 Basis Technology Corp. + * Copyright 2013-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -182,15 +182,40 @@ public class FileSize implements AutopsyVisitableItem { public void propertyChange(PropertyChangeEvent evt) { String eventType = evt.getPropertyName(); - // new file was added if (eventType.equals(IngestManager.IngestModuleEvent.CONTENT_CHANGED.toString())) { - // @@@ could check the size here and only fire off updates if we know the file meets the min size criteria - update(); + /** + * Checking for a current case is a stop gap measure + * until a different way of handling the closing of + * cases is worked out. Currently, remote events may be + * received for a case that is already closed. + */ + try { + // new file was added + // @@@ could check the size here and only fire off updates if we know the file meets the min size criteria + Case.getCurrentCase(); + update(); + } catch (IllegalStateException notUsed) { + /** + * Case is closed, do nothing. + */ + } } else if (eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString()) - || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString())) { - update(); - } else if (eventType.equals(Case.Events.DATA_SOURCE_ADDED.toString())) { - update(); + || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString()) + || eventType.equals(Case.Events.DATA_SOURCE_ADDED.toString())) { + /** + * Checking for a current case is a stop gap measure + * until a different way of handling the closing of + * cases is worked out. Currently, remote events may be + * received for a case that is already closed. + */ + try { + Case.getCurrentCase(); + update(); + } catch (IllegalStateException notUsed) { + /** + * Case is closed, do nothing. + */ + } } else if (eventType.equals(Case.Events.CURRENT_CASE.toString())) { // case was closed. Remove listeners so that we don't get called with a stale case handle if (evt.getNewValue() == null) { @@ -348,11 +373,7 @@ public class FileSize implements AutopsyVisitableItem { @Override protected boolean createKeys(List list) { - List l = runFsQuery(); - if (l == null) { - return false; - } - list.addAll(l); + list.addAll(runFsQuery()); return true; } @@ -371,8 +392,7 @@ public class FileSize implements AutopsyVisitableItem { break; default: - logger.log(Level.SEVERE, "Unsupported filter type to get files by size: {0}", filter); //NON-NLS - return null; + throw new IllegalArgumentException("Unsupported filter type to get files by size: " + filter); //NON-NLS } // ignore unalloc block files query = query + " AND (type != " + TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS.getFileType() + ")"; //NON-NLS @@ -383,19 +403,15 @@ public class FileSize implements AutopsyVisitableItem { private List runFsQuery() { List ret = new ArrayList<>(); - String query = makeQuery(filter); - if (query == null) { - return null; - } - try { + String query = makeQuery(filter); + ret = skCase.findAllFilesWhere(query); - } catch (TskCoreException e) { - logger.log(Level.SEVERE, "Error getting files for the file size view using: " + query, e); //NON-NLS + } catch (Exception e) { + logger.log(Level.SEVERE, "Error getting files for the file size view: " + e.getMessage()); //NON-NLS } return ret; - } /** diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypeNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypeNode.java index f6d9713f58..f6b97a74e1 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypeNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypeNode.java @@ -220,9 +220,9 @@ public class FileTypeNode extends DisplayableItemNode { if (UserPreferences.hideKnownFilesInViewsTree()) { query.append(" AND (known IS NULL OR known != ").append(TskData.FileKnown.KNOWN.getFileKnownValue()).append(")"); //NON-NLS } - query.append(" AND (0"); //NON-NLS + query.append(" AND (NULL"); //NON-NLS for (String s : filter.getFilter()) { - query.append(" OR name LIKE '%").append(s).append("'"); //NON-NLS + query.append(" OR LOWER(name) LIKE LOWER('%").append(s).append("')"); //NON-NLS } query.append(')'); return query.toString(); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesNode.java index 863d9aab5e..a268489167 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesNode.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2014 Basis Technology Corp. + * Copyright 2011-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -152,15 +152,24 @@ public class FileTypesNode extends DisplayableItemNode { @Override public void propertyChange(PropertyChangeEvent evt) { String eventType = evt.getPropertyName(); - - // new file was added - if (eventType.equals(IngestManager.IngestModuleEvent.CONTENT_CHANGED.toString())) { - update(); - } else if (eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString()) - || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString())) { - update(); - } else if (eventType.equals(Case.Events.DATA_SOURCE_ADDED.toString())) { - update(); + if (eventType.equals(IngestManager.IngestModuleEvent.CONTENT_CHANGED.toString()) + || eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString()) + || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString()) + || eventType.equals(Case.Events.DATA_SOURCE_ADDED.toString())) { + /** + * Checking for a current case is a stop gap measure + * until a different way of handling the closing of + * cases is worked out. Currently, remote events may be + * received for a case that is already closed. + */ + try { + Case.getCurrentCase(); + update(); + } catch (IllegalStateException notUsed) { + /** + * Case is closed, do nothing. + */ + } } else if (eventType.equals(Case.Events.CURRENT_CASE.toString())) { // case was closed. Remove listeners so that we don't get called with a stale case handle if (evt.getNewValue() == null) { diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java b/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java index d8b9d8e7c5..6f75877c94 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2014 Basis Technology Corp. + * Copyright 2011-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -184,14 +184,45 @@ public class HashsetHits implements AutopsyVisitableItem { @Override public void propertyChange(PropertyChangeEvent evt) { String eventType = evt.getPropertyName(); - if (eventType.equals(IngestManager.IngestModuleEvent.DATA_ADDED.toString())) { - if (((ModuleDataEvent) evt.getOldValue()).getArtifactType() == ARTIFACT_TYPE.TSK_HASHSET_HIT) { - hashsetResults.update(); + /** + * Checking for a current case is a stop gap measure until a + * different way of handling the closing of cases is worked + * out. Currently, remote events may be received for a case + * that is already closed. + */ + try { + Case.getCurrentCase(); + /** + * Due to some unresolved issues with how cases are + * closed, it is possible for the event to have a null + * oldValue if the event is a remote event. + */ + ModuleDataEvent eventData = (ModuleDataEvent) evt.getOldValue(); + if (null != eventData && eventData.getArtifactType() == ARTIFACT_TYPE.TSK_HASHSET_HIT) { + hashsetResults.update(); + } + } catch (IllegalStateException notUsed) { + /** + * Case is closed, do nothing. + */ } } else if (eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString()) || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString())) { - hashsetResults.update(); + /** + * Checking for a current case is a stop gap measure until a + * different way of handling the closing of cases is worked + * out. Currently, remote events may be received for a case + * that is already closed. + */ + try { + Case.getCurrentCase(); + hashsetResults.update(); + } catch (IllegalStateException notUsed) { + /** + * Case is closed, do nothing. + */ + } } else if (eventType.equals(Case.Events.CURRENT_CASE.toString())) { // case was closed. Remove listeners so that we don't get called with a stale case handle if (evt.getNewValue() == null) { diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/InterestingHits.java b/Core/src/org/sleuthkit/autopsy/datamodel/InterestingHits.java index 0e7e2ba324..c4c0080679 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/InterestingHits.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/InterestingHits.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011 Basis Technology Corp. + * Copyright 2011-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -175,15 +175,47 @@ public class InterestingHits implements AutopsyVisitableItem { @Override public void propertyChange(PropertyChangeEvent evt) { String eventType = evt.getPropertyName(); - if (eventType.equals(IngestManager.IngestModuleEvent.DATA_ADDED.toString())) { - if ((((ModuleDataEvent) evt.getOldValue()).getArtifactType() == BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_ARTIFACT_HIT) - || (((ModuleDataEvent) evt.getOldValue()).getArtifactType() == BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT)) { - interestingResults.update(); + /** + * Checking for a current case is a stop gap measure until a + * different way of handling the closing of cases is worked + * out. Currently, remote events may be received for a case + * that is already closed. + */ + try { + Case.getCurrentCase(); + /** + * Even with the check above, it is still possible that + * the case will be closed in a different thread before + * this code executes. If that happens, it is possible + * for the event to have a null oldValue. + */ + ModuleDataEvent eventData = (ModuleDataEvent) evt.getOldValue(); + if (null != eventData && (eventData.getArtifactType() == BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_ARTIFACT_HIT + || eventData.getArtifactType() == BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT)) { + interestingResults.update(); + } + } catch (IllegalStateException notUsed) { + /** + * Case is closed, do nothing. + */ } } else if (eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString()) || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString())) { - interestingResults.update(); + /** + * Checking for a current case is a stop gap measure until a + * different way of handling the closing of cases is worked + * out. Currently, remote events may be received for a case + * that is already closed. + */ + try { + Case.getCurrentCase(); + interestingResults.update(); + } catch (IllegalStateException notUsed) { + /** + * Case is closed, do nothing. + */ + } } else if (eventType.equals(Case.Events.CURRENT_CASE.toString())) { // case was closed. Remove listeners so that we don't get called with a stale case handle if (evt.getNewValue() == null) { diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/KeywordHits.java b/Core/src/org/sleuthkit/autopsy/datamodel/KeywordHits.java index 68886f3b5c..9ff5ad2d5d 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/KeywordHits.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/KeywordHits.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2014 Basis Technology Corp. + * Copyright 2011-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -247,14 +247,46 @@ public class KeywordHits implements AutopsyVisitableItem { @Override public void propertyChange(PropertyChangeEvent evt) { String eventType = evt.getPropertyName(); - if (eventType.equals(IngestManager.IngestModuleEvent.DATA_ADDED.toString())) { - if (((ModuleDataEvent) evt.getOldValue()).getArtifactType() == BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT) { - keywordResults.update(); + /** + * Checking for a current case is a stop gap measure until a + * different way of handling the closing of cases is worked + * out. Currently, remote events may be received for a case + * that is already closed. + */ + try { + Case.getCurrentCase(); + /** + * Even with the check above, it is still possible that + * the case will be closed in a different thread before + * this code executes. If that happens, it is possible + * for the event to have a null oldValue. + */ + ModuleDataEvent eventData = (ModuleDataEvent) evt.getOldValue(); + if (null != eventData && eventData.getArtifactType() == BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT) { + keywordResults.update(); + } + } catch (IllegalStateException notUsed) { + /** + * Case is closed, do nothing. + */ } } else if (eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString()) || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString())) { - keywordResults.update(); + /** + * Checking for a current case is a stop gap measure until a + * different way of handling the closing of cases is worked + * out. Currently, remote events may be received for a case + * that is already closed. + */ + try { + Case.getCurrentCase(); + keywordResults.update(); + } catch (IllegalStateException notUsed) { + /** + * Case is closed, do nothing. + */ + } } else if (eventType.equals(Case.Events.CURRENT_CASE.toString())) { // case was closed. Remove listeners so that we don't get called with a stale case handle if (evt.getNewValue() == null) { diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/RecentFilesChildren.java b/Core/src/org/sleuthkit/autopsy/datamodel/RecentFilesChildren.java index 46739ee2a7..391a49f757 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/RecentFilesChildren.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/RecentFilesChildren.java @@ -20,7 +20,6 @@ package org.sleuthkit.autopsy.datamodel; import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Statement; import java.util.Arrays; import java.util.Calendar; import java.util.List; @@ -78,7 +77,7 @@ class RecentFilesChildren extends ChildFactory { //TODO add a generic query to SleuthkitCase private String createMaxQuery(String attr) { - return "SELECT MAX(" + attr + ") from tsk_files WHERE " + attr + " < " + System.currentTimeMillis() / 1000; //NON-NLS + return "SELECT MAX(" + attr + ") FROM tsk_files WHERE " + attr + " < " + System.currentTimeMillis() / 1000; //NON-NLS } @SuppressWarnings("deprecation") @@ -87,6 +86,7 @@ class RecentFilesChildren extends ChildFactory { try (CaseDbQuery dbQuery = skCase.executeQuery(query)) { ResultSet resultSet = dbQuery.getResultSet(); + resultSet.next(); result = resultSet.getLong(1); } catch (TskCoreException | SQLException ex) { logger.log(Level.WARNING, "Couldn't get recent files results: ", ex); //NON-NLS diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/Reports.java b/Core/src/org/sleuthkit/autopsy/datamodel/Reports.java index d9da8d8b1b..013aa71ec0 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/Reports.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/Reports.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014 Basis Technology Corp. + * Copyright 2011-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -101,8 +101,21 @@ public final class Reports implements AutopsyVisitableItem { @Override public void propertyChange(PropertyChangeEvent evt) { String eventType = evt.getPropertyName(); - if (eventType.equals(Case.Events.REPORT_ADDED.toString()) || eventType.equals(Case.Events.REPORT_DELETED.toString())) { - ReportNodeFactory.this.refresh(true); + if (eventType.equals(Case.Events.REPORT_ADDED.toString())) { + /** + * Checking for a current case is a stop gap measure + * until a different way of handling the closing of + * cases is worked out. Currently, remote events may be + * received for a case that is already closed. + */ + try { + Case.getCurrentCase(); + ReportNodeFactory.this.refresh(true); + } catch (IllegalStateException notUsed) { + /** + * Case is closed, do nothing. + */ + } } } }); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/Tags.java b/Core/src/org/sleuthkit/autopsy/datamodel/Tags.java index b09c11c9c9..aa9eec9b5f 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/Tags.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/Tags.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013 Basis Technology Corp. + * Copyright 2011-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -120,11 +120,38 @@ public class Tags implements AutopsyVisitableItem { || eventType.equals(Case.Events.BLACKBOARD_ARTIFACT_TAG_DELETED.toString()) || eventType.equals(Case.Events.CONTENT_TAG_ADDED.toString()) || eventType.equals(Case.Events.CONTENT_TAG_DELETED.toString())) { - refresh(true); - tagResults.update(); - } else if (eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString()) || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString())) { - refresh(true); - tagResults.update(); + /** + * Checking for a current case is a stop gap measure until a + * different way of handling the closing of cases is worked + * out. Currently, remote events may be received for a case + * that is already closed. + */ + try { + Case.getCurrentCase(); + refresh(true); + tagResults.update(); + } catch (IllegalStateException notUsed) { + /** + * Case is closed, do nothing. + */ + } + } else if (eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString()) + || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString())) { + /** + * Checking for a current case is a stop gap measure until a + * different way of handling the closing of cases is worked + * out. Currently, remote events may be received for a case + * that is already closed. + */ + try { + Case.getCurrentCase(); + refresh(true); + tagResults.update(); + } catch (IllegalStateException notUsed) { + /** + * Case is closed, do nothing. + */ + } } else if (eventType.equals(Case.Events.CURRENT_CASE.toString())) { // case was closed. Remove listeners so that this can be garbage collected if (evt.getNewValue() == null) { @@ -435,7 +462,7 @@ public class Tags implements AutopsyVisitableItem { BlackboardArtifactTagNodeFactory(TagName tagName) { super(); this.tagName = tagName; - tagResults.addObserver(this); + tagResults.addObserver(this); } @Override @@ -454,7 +481,8 @@ public class Tags implements AutopsyVisitableItem { // The blackboard artifact tags to be wrapped are used as the keys. return new BlackboardArtifactTagNode(key); } - @Override + + @Override public void update(Observable o, Object arg) { refresh(true); } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/VolumeNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/VolumeNode.java index 4e8e2e24fe..eb4b76a233 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/VolumeNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/VolumeNode.java @@ -18,13 +18,21 @@ */ package org.sleuthkit.autopsy.datamodel; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.List; import javax.swing.Action; +import org.openide.nodes.Children; import org.openide.nodes.Sheet; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.directorytree.ExplorerNodeActionVisitor; import org.sleuthkit.autopsy.directorytree.NewWindowViewAction; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.autopsy.ingest.ModuleContentEvent; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.VirtualDirectory; import org.sleuthkit.datamodel.Volume; /** @@ -60,8 +68,46 @@ public class VolumeNode extends AbstractContentNode { this.setDisplayName(tempVolName); this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/vol-icon.png"); //NON-NLS + // Listen for ingest events so that we can detect new added files (e.g. carved) + IngestManager.getInstance().addIngestModuleEventListener(pcl); + } + private final PropertyChangeListener pcl = (PropertyChangeEvent evt) -> { + String eventType = evt.getPropertyName(); + + // See if the new file is a child of ours + if (eventType.equals(IngestManager.IngestModuleEvent.CONTENT_CHANGED.toString())) { + if ((evt.getOldValue() instanceof ModuleContentEvent) == false) { + return; + } + ModuleContentEvent moduleContentEvent = (ModuleContentEvent) evt.getOldValue(); + if ((moduleContentEvent.getSource() instanceof Content) == false) { + return; + } + Content newContent = (Content) moduleContentEvent.getSource(); + + try { + Content parent = newContent.getParent(); + if (parent != null) { + // Is this a new carved file? + if (parent.getName().equals(VirtualDirectory.NAME_CARVED)) { + // Was this new carved file produced from this volume? + if (parent.getParent().getId() == getContent().getId()) { + Children children = getChildren(); + if (children != null) { + ((ContentChildren) children).refreshChildren(); + children.getNodesCount(); + } + } + } + } + } catch (TskCoreException ex) { + // Do nothing. + } + } + }; + /** * Right click action for volume node * diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java b/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java index ac9594c776..e4c34ebce9 100755 --- a/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java @@ -239,6 +239,11 @@ public class DataResultFilterNode extends FilterNode { actions.add(AddContentTagAction.getInstance()); actions.add(AddBlackboardArtifactTagAction.getInstance()); actions.addAll(ContextMenuExtensionPoint.getActions()); + } else { + // There's no specific file associated with the artifact, but + // we can still tag the artifact itself + actions.add(null); + actions.add(AddBlackboardArtifactTagAction.getInstance()); } return actions; } diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeTopComponent.java b/Core/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeTopComponent.java index a5a2468cf7..da49e1546f 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeTopComponent.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeTopComponent.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2014 Basis Technology Corp. + * Copyright 2011-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,13 +27,13 @@ import java.beans.PropertyVetoException; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.logging.Level; import java.util.prefs.PreferenceChangeEvent; import java.util.prefs.PreferenceChangeListener; import javax.swing.Action; -import javax.swing.JPanel; import javax.swing.SwingUtilities; import javax.swing.tree.TreeSelectionModel; import org.openide.explorer.ExplorerManager; @@ -49,8 +49,10 @@ import org.openide.util.NbBundle; import org.openide.windows.TopComponent; import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.core.RuntimeProperties; import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.corecomponentinterfaces.BlackboardResultViewer; +import org.sleuthkit.autopsy.corecomponentinterfaces.CoreComponentControl; import org.sleuthkit.autopsy.corecomponentinterfaces.DataExplorer; import org.sleuthkit.autopsy.corecomponents.DataResultTopComponent; import org.sleuthkit.autopsy.corecomponents.TableFilterNode; @@ -83,21 +85,14 @@ import org.sleuthkit.datamodel.TskException; // Registered as a service provider for DataExplorer in layer.xml public final class DirectoryTreeTopComponent extends TopComponent implements DataExplorer, ExplorerManager.Provider, BlackboardResultViewer { - private transient ExplorerManager em = new ExplorerManager(); + private final transient ExplorerManager em = new ExplorerManager(); private static DirectoryTreeTopComponent instance; - private DataResultTopComponent dataResult = new DataResultTopComponent(true, NbBundle.getMessage(this.getClass(), + private final DataResultTopComponent dataResult = new DataResultTopComponent(true, NbBundle.getMessage(this.getClass(), "DirectoryTreeTopComponent.title.text")); - private LinkedList backList; - private LinkedList forwardList; - /** - * path to the icon used by the component and its open action - */ -// static final String ICON_PATH = "SET/PATH/TO/ICON/HERE"; + private final LinkedList backList; + private final LinkedList forwardList; private static final String PREFERRED_ID = "DirectoryTreeTopComponent"; //NON-NLS - private PropertyChangeSupport pcs; - // for error handling - private JPanel caller; - private String className = this.getClass().toString(); + private final PropertyChangeSupport pcs; private static final Logger logger = Logger.getLogger(DirectoryTreeTopComponent.class.getName()); private RootContentChildren contentChildren; @@ -143,7 +138,7 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat } } }); - Case.addPropertyChangeListener(this); + Case.addEventSubscriber(new HashSet<>(Arrays.asList(Case.Events.CURRENT_CASE.toString(), Case.Events.DATA_SOURCE_ADDED.toString())), this); this.em.addPropertyChangeListener(this); IngestManager.getInstance().addIngestJobEventListener(this); IngestManager.getInstance().addIngestModuleEventListener(this); @@ -529,70 +524,71 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat */ @Override public void propertyChange(PropertyChangeEvent evt) { - String changed = evt.getPropertyName(); - Object oldValue = evt.getOldValue(); - Object newValue = evt.getNewValue(); + if (RuntimeProperties.coreComponentsAreActive()) { + String changed = evt.getPropertyName(); + if (changed.equals(Case.Events.CURRENT_CASE.toString())) { // changed current case + // When a case is closed, the old value of this property is the + // closed Case object and the new value is null. When a case is + // opened, the old value is null and the new value is the new Case + // object. + // @@@ This needs to be revisited. Perhaps case closed and case + // opened events instead of property change events would be a better + // solution. Either way, more probably needs to be done to clean up + // data model objects when a case is closed. + if (evt.getOldValue() != null && evt.getNewValue() == null) { + // The current case has been closed. Reset the ExplorerManager. + SwingUtilities.invokeLater(() -> { + Node emptyNode = new AbstractNode(Children.LEAF); + em.setRootContext(emptyNode); + }); + } else if (evt.getNewValue() != null) { + // A new case has been opened. Reset the ExplorerManager. + Case newCase = (Case) evt.getNewValue(); + final String newCaseName = newCase.getName(); + SwingUtilities.invokeLater(() -> { + em.getRootContext().setName(newCaseName); + em.getRootContext().setDisplayName(newCaseName); - // change in the case name - if (changed.equals(Case.Events.NAME.toString())) { - // set the main title of the window - String oldCaseName = oldValue.toString(); - String newCaseName = newValue.toString(); - - // update the case name - if ((!oldCaseName.equals("")) && (!newCaseName.equals(""))) { - // change the root name and display name - em.getRootContext().setName(newCaseName); - em.getRootContext().setDisplayName(newCaseName); - } - } // changed current case - else if (changed.equals(Case.Events.CURRENT_CASE.toString())) { - // When a case is closed, the old value of this property is the - // closed Case object and the new value is null. When a case is - // opened, the old value is null and the new value is the new Case - // object. - // @@@ This needs to be revisited. Perhaps case closed and case - // opened events instead of property change events would be a better - // solution. Either way, more probably needs to be done to clean up - // data model objects when a case is closed. - if (oldValue != null && newValue == null) { - // The current case has been closed. Reset the ExplorerManager. - Node emptyNode = new AbstractNode(Children.LEAF); - em.setRootContext(emptyNode); - } else if (newValue != null) { - // A new case has been opened. Reset the forward and back - // buttons. Note that a call to CoreComponentControl.openCoreWindows() - // by the new Case object will lead to a componentOpened() call - // that will repopulate the tree. - // @@@ The repopulation of the tree in this fashion also merits - // reconsideration. - resetHistory(); - } - } // if the image is added to the case - else if (changed.equals(Case.Events.DATA_SOURCE_ADDED.toString())) { - // we don't need to do anything in here. - // DataSourcesNode is listening for these events and updates itself - } // change in node selection - else if (changed.equals(ExplorerManager.PROP_SELECTED_NODES)) { - respondSelection((Node[]) oldValue, (Node[]) newValue); - } else if (changed.equals(IngestManager.IngestModuleEvent.DATA_ADDED.toString())) { - // nothing to do here. - // all nodes should be listening for these events and update accordingly. - } else if (changed.equals(IngestManager.IngestJobEvent.COMPLETED.toString()) - || changed.equals(IngestManager.IngestJobEvent.CANCELLED.toString())) { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - refreshDataSourceTree(); + // Reset the forward and back + // buttons. Note that a call to CoreComponentControl.openCoreWindows() + // by the new Case object will lead to a componentOpened() call + // that will repopulate the tree. + // @@@ The repopulation of the tree in this fashion also merits + // reconsideration. + resetHistory(); + }); } - }); - } else if (changed.equals(IngestManager.IngestModuleEvent.CONTENT_CHANGED.toString())) { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - refreshDataSourceTree(); + } // if the image is added to the case + else if (changed.equals(Case.Events.DATA_SOURCE_ADDED.toString())) { + /** + * Checking for a current case is a stop gap measure until a + * different way of handling the closing of cases is worked out. + * Currently, remote events may be received for a case that is + * already closed. + */ + try { + Case currentCase = Case.getCurrentCase(); + // We only need to trigger openCoreWindows() when the + // first data source is added. + if (currentCase.getDataSources().size() == 1) { + SwingUtilities.invokeLater(() -> { + CoreComponentControl.openCoreWindows(); + }); + } + } catch (IllegalStateException | TskCoreException notUsed) { + /** + * Case is closed, do nothing. + */ } - }); + } // change in node selection + else if (changed.equals(ExplorerManager.PROP_SELECTED_NODES)) { + SwingUtilities.invokeLater(() -> { + respondSelection((Node[]) evt.getOldValue(), (Node[]) evt.getNewValue()); + }); + } else if (changed.equals(IngestManager.IngestModuleEvent.DATA_ADDED.toString())) { + // nothing to do here. + // all nodes should be listening for these events and update accordingly. + } } } @@ -773,7 +769,7 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat Node imagesNode = imagesNodeOrig.getNode(); - DataSourcesNode.DataSourcesNodeChildren contentRootChildren = (DataSourcesNode.DataSourcesNodeChildren) imagesNode.getChildren(); + DataSourcesNode.DataSourcesNodeChildren contentRootChildren = (DataSourcesNode.DataSourcesNodeChildren) imagesNode.getChildren(); contentRootChildren.refreshContentKeys(); //final TreeView tree = getTree(); @@ -782,56 +778,6 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat } - /** - * Refreshes the nodes in the tree to reflect updates in the database should - * be called in the gui thread - */ -// public void refreshResultsTree(final BlackboardArtifact.ARTIFACT_TYPE... types) { -// //save current selection -// Node selectedNode = getSelectedNode(); -// final String[] selectedPath = NodeOp.createPath(selectedNode, em.getRootContext()); -// -// //TODO: instead, we should choose a specific key to refresh? Maybe? -// //contentChildren.refreshKeys(); -// -// Children dirChilds = em.getRootContext().getChildren(); -// -// Node results = dirChilds.findChild(ResultsNode.NAME); -// if (results == null) { -// logger.log(Level.SEVERE, "Cannot find Results filter node, won't refresh the bb tree"); //NON-NLS -// return; -// } -// -// OriginalNode original = results.getLookup().lookup(OriginalNode.class); -// ResultsNode resultsNode = (ResultsNode) original.getNode(); -// RootContentChildren resultsNodeChilds = (RootContentChildren) resultsNode.getChildren(); -// resultsNodeChilds.refreshKeys(types); -// -// -// final TreeView tree = getTree(); -// // @@@ tree.expandNode(results); -// -// Children resultsChilds = results.getChildren(); -// if (resultsChilds == null) { -// return; -// } -// -// Node childNode = resultsChilds.findChild(KeywordHits.NAME); -// if (childNode == null) { -// return; -// } -// // @@@tree.expandNode(childNode); -// -// childNode = resultsChilds.findChild(ExtractedContent.NAME); -// if (childNode == null) { -// return; -// } -// tree.expandNode(childNode); -// -// //restores selection if it was under the Results node -// //@@@ setSelectedNode(selectedPath, ResultsNode.NAME); -// -// } /** * Set the selected node using a path to a previously selected node. * @@ -1002,10 +948,6 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat new BlackboardArtifactNode(art)).actionPerformed(null); } -// private class HistoryManager { -// private Stack past, future; -// -// } @Override public void addOnFinishedListener(PropertyChangeListener l) { DirectoryTreeTopComponent.this.addPropertyChangeListener(l); diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ExtractAction.java b/Core/src/org/sleuthkit/autopsy/directorytree/ExtractAction.java index 95563ac502..a9b82e4680 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/ExtractAction.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ExtractAction.java @@ -24,8 +24,6 @@ import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; -import java.util.concurrent.CancellationException; -import java.util.concurrent.ExecutionException; import java.util.logging.Level; import org.openide.util.NbBundle; @@ -101,7 +99,7 @@ public final class ExtractAction extends AbstractAction { JFileChooser fileChooser = new JFileChooser(); fileChooser.setCurrentDirectory(new File(Case.getCurrentCase().getExportDirectory())); // If there is an attribute name, change the ":". Otherwise the extracted file will be hidden - fileChooser.setSelectedFile(new File(selectedFile.getName().replace(':', '_'))); + fileChooser.setSelectedFile(new File(FileUtil.escapeFileName(selectedFile.getName()))); if (fileChooser.showSaveDialog((Component) e.getSource()) == JFileChooser.APPROVE_OPTION) { ArrayList fileExtractionTasks = new ArrayList<>(); fileExtractionTasks.add(new FileExtractionTask(selectedFile, fileChooser.getSelectedFile())); @@ -136,7 +134,7 @@ public final class ExtractAction extends AbstractAction { ArrayList fileExtractionTasks = new ArrayList<>(); for (AbstractFile source : selectedFiles) { // If there is an attribute name, change the ":". Otherwise the extracted file will be hidden - fileExtractionTasks.add(new FileExtractionTask(source, new File(destinationFolder, source.getId() + "-" + source.getName().replace(':', '_')))); + fileExtractionTasks.add(new FileExtractionTask(source, new File(destinationFolder, source.getId() + "-" + FileUtil.escapeFileName(source.getName())))); } runExtractionTasks(e, fileExtractionTasks); } diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ExtractUnallocAction.java b/Core/src/org/sleuthkit/autopsy/directorytree/ExtractUnallocAction.java index 02ad08379d..d4738fb675 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/ExtractUnallocAction.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ExtractUnallocAction.java @@ -121,7 +121,7 @@ final class ExtractUnallocAction extends AbstractAction { } }; - fc.setCurrentDirectory(new File(Case.getCurrentCase().getCaseDirectory() + File.separator + "Export")); + fc.setCurrentDirectory(new File(Case.getCurrentCase().getExportDirectory())); fc.setDialogTitle( NbBundle.getMessage(this.getClass(), "ExtractUnallocAction.dlgTitle.selectDirToSaveTo.msg")); fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); @@ -503,7 +503,7 @@ final class ExtractUnallocAction extends AbstractAction { this.ImageId = img.getId(); this.ImageName = img.getName(); this.FileName = this.ImageName + "-Unalloc-" + this.ImageId + "-" + 0 + ".dat"; //NON-NLS - this.FileInstance = new File(Case.getCurrentCase().getCaseDirectory() + File.separator + "Export" + File.separator + this.FileName); + this.FileInstance = new File(Case.getCurrentCase().getExportDirectory() + File.separator + this.FileName); this.SizeInBytes = calcSizeInBytes(); } @@ -523,7 +523,7 @@ final class ExtractUnallocAction extends AbstractAction { this.ImageId = 0; } this.FileName = this.ImageName + "-Unalloc-" + this.ImageId + "-" + VolumeId + ".dat"; //NON-NLS - this.FileInstance = new File(Case.getCurrentCase().getCaseDirectory() + File.separator + "Export" + File.separator + this.FileName); + this.FileInstance = new File(Case.getCurrentCase().getExportDirectory() + File.separator + this.FileName); this.llf = getUnallocFiles(volu); Collections.sort(llf, new SortObjId()); this.SizeInBytes = calcSizeInBytes(); diff --git a/Core/src/org/sleuthkit/autopsy/events/AutopsyEvent.java b/Core/src/org/sleuthkit/autopsy/events/AutopsyEvent.java index fdaf727867..63a0222e54 100644 --- a/Core/src/org/sleuthkit/autopsy/events/AutopsyEvent.java +++ b/Core/src/org/sleuthkit/autopsy/events/AutopsyEvent.java @@ -1,19 +1,98 @@ /* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. + * Autopsy Forensic Browser + * + * Copyright 2015 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.events; import java.beans.PropertyChangeEvent; +import java.io.Serializable; +import javax.annotation.concurrent.Immutable; /** - * This is a place holder class to be overwritten by the version in the - * Collaborative branch. + * A base class for events to be published to registered subscribers on both + * this Autopsy node and other Autopsy nodes. The class extends + * PropertyChangeEvent to integrate with legacy use of JavaBeans + * PropertyChangeEvents and PropertyChangeListeners as an application event + * system, and implements Serializable to allow it to be published over a + * network in serialized form. */ -abstract class AutopsyEvent extends PropertyChangeEvent { +@Immutable +public class AutopsyEvent extends PropertyChangeEvent implements Serializable { - AutopsyEvent(Object source, String propertyName, Object oldValue, Object newValue) { - super(source, propertyName, oldValue, newValue); + private static final long serialVersionUID = 1L; + private SourceType sourceType; + + /** + * Events have a source field set to local or remote to allow event + * subscribers to filter events by source type. + */ + public enum SourceType { + + LOCAL, + REMOTE + }; + + /** + * Constructs an event that can be published to registered subscribers on + * both this Autopsy node and other Autopsy nodes. + * + * @param eventName The event name. + * @param oldValue The "old" value to associate with the event. May be + * null. + * @param newValue The "new" value to associate with the event. May be + * null. + */ + public AutopsyEvent(String eventName, Object oldValue, Object newValue) { + super(SourceType.LOCAL.toString(), eventName, oldValue, newValue); + this.sourceType = SourceType.LOCAL; } + + /** + * Gets the source type (local or remote). + * + * @return SourceType The source type of the event, local or remote. + */ + public SourceType getSourceType() { + return sourceType; + } + + /** + * Gets the source type (local or remote) as a string. This is for clients + * that do not have access to the AutopsyEvent type, and is necessary + * because the events package is not currently a public package within the + * Autopsy-Core NetBeans Module (NBM). + * + * @return A string, either "LOCAL" or "REMOTE", as an Object. + */ + @Override + public Object getSource() { + return sourceType.toString(); + } + + /** + * Sets the source type (local or remote). This field is mutable in this way + * to allow an event to be published both locally and remotely without + * requiring the construction of two separate objects. It is for use by the + * event publishing classes within this package only. + * + * @param sourceType The source type of the event, local or remote. + */ + void setSourceType(SourceType sourceType) { + this.sourceType = sourceType; + } + } diff --git a/Core/src/org/sleuthkit/autopsy/events/TagAddedEvent.java b/Core/src/org/sleuthkit/autopsy/events/AutopsyEventException.java similarity index 54% rename from Core/src/org/sleuthkit/autopsy/events/TagAddedEvent.java rename to Core/src/org/sleuthkit/autopsy/events/AutopsyEventException.java index 217af496cc..5f1ce611ed 100644 --- a/Core/src/org/sleuthkit/autopsy/events/TagAddedEvent.java +++ b/Core/src/org/sleuthkit/autopsy/events/AutopsyEventException.java @@ -19,28 +19,37 @@ package org.sleuthkit.autopsy.events; import javax.annotation.concurrent.Immutable; -import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.datamodel.Tag; /** - * Base Class for events that are fired when a Tag is added + * Provides a generic events system exception for clients of the events package. */ @Immutable -abstract class TagAddedEvent extends TagEvent { +public final class AutopsyEventException extends Exception { - protected TagAddedEvent(String propertyName, T newValue) { - super(Case.class, propertyName, null, newValue); + /** + * Constructs a new exception with null as its message. + */ + AutopsyEventException() { + super(); } /** - * get the Tag that was added + * Constructs a new exception with the specified message. * - * @return the tTag + * @param message The message. */ - @SuppressWarnings("unchecked") - @Override - public T getTag() { - return (T) getNewValue(); + AutopsyEventException(String message) { + super(message); + } + + /** + * Constructs a new exception with the specified message and cause. + * + * @param message The message. + * @param cause The cause. + */ + AutopsyEventException(String message, Throwable cause) { + super(message, cause); } } diff --git a/Core/src/org/sleuthkit/autopsy/events/AutopsyEventPublisher.java b/Core/src/org/sleuthkit/autopsy/events/AutopsyEventPublisher.java new file mode 100644 index 0000000000..80c23d924f --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/events/AutopsyEventPublisher.java @@ -0,0 +1,170 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-2015 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.events; + +import java.beans.PropertyChangeListener; +import java.net.URISyntaxException; +import java.util.Set; +import java.util.logging.Level; +import javax.jms.JMSException; +import org.sleuthkit.autopsy.core.UserPreferencesException; +import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.autopsy.coreutils.Logger; + +/** + * Provides thread-safe support for publishing events to registered subscribers + * on both this Autopsy node and other Autopsy nodes. Subscribers are + * constrained to be PropertyChangeListeners to integrate with the legacy use of + * JavaBeans PropertyChangeEvents and PropertyChangeListeners as an application + * event system. + */ +public final class AutopsyEventPublisher { + + /** + * Composed of thread-safe objects. + */ + private static final Logger logger = Logger.getLogger(AutopsyEventPublisher.class.getName()); + private final LocalEventPublisher localPublisher; + private RemoteEventPublisher remotePublisher; + + /** + * Constructs an object for publishing events to registered subscribers on + * both this Autopsy node and other Autopsy nodes. Communication with other + * nodes is not turned on by default - call openRemoteEventChannel() after + * construction. + */ + public AutopsyEventPublisher() { + localPublisher = new LocalEventPublisher(); + } + + /** + * Opens the event channel used for publishing events to and receiving + * events from other Autopsy nodes. Only one channel may be open at a time. + * + * @param channelName The name of the event channel. + * + * @throws AutopsyEventException if the channel was not opened. + */ + public void openRemoteEventChannel(String channelName) throws AutopsyEventException { + if (null != remotePublisher) { + closeRemoteEventChannel(); + } + try { + remotePublisher = new RemoteEventPublisher(channelName, localPublisher, UserPreferences.getMessageServiceConnectionInfo()); + } catch (URISyntaxException | JMSException ex) { + String message = "Failed to open remote event channel"; //NON-NLS + logger.log(Level.SEVERE, message, ex); + throw new AutopsyEventException(message, ex); + } catch (UserPreferencesException ex) { + String message = "Error accessing messaging service connection info"; //NON-NLS + logger.log(Level.SEVERE, message, ex); + throw new AutopsyEventException(message, ex); + } + } + + /** + * Closes the event channel used for publishing events to and receiving + * events from other Autopsy nodes. + */ + public void closeRemoteEventChannel() { + if (null != remotePublisher) { + try { + remotePublisher.stop(); + } catch (JMSException ex) { + logger.log(Level.SEVERE, "Error closing remote event channel", ex); //NON-NLS + } + remotePublisher = null; + } + } + + /** + * Adds an event subscriber to this publisher. + * + * @param eventNames The events the subscriber is interested in. + * @param subscriber The subscriber to add. + */ + public void addSubscriber(Set eventNames, PropertyChangeListener subscriber) { + localPublisher.addSubscriber(eventNames, subscriber); + } + + /** + * Adds an event subscriber to this publisher. + * + * @param eventName The event the subscriber is interested in. + * @param subscriber The subscriber to add. + */ + public void addSubscriber(String eventName, PropertyChangeListener subscriber) { + localPublisher.addSubscriber(eventName, subscriber); + } + + /** + * Removes an event subscriber from this publisher. + * + * @param eventNames The events the subscriber is no longer interested in. + * @param subscriber The subscriber to remove. + */ + public void removeSubscriber(Set eventNames, PropertyChangeListener subscriber) { + localPublisher.removeSubscriber(eventNames, subscriber); + } + + /** + * Removes an event subscriber from this publisher. + * + * @param eventName The event the subscriber is no longer interested in. + * @param subscriber The subscriber to remove. + */ + public void removeSubscriber(String eventName, PropertyChangeListener subscriber) { + localPublisher.removeSubscriber(eventName, subscriber); + } + + /** + * Publishes an event to this Autopsy node and other Autopsy nodes. + * + * @param event The event to publish. + */ + public void publish(AutopsyEvent event) { + publishLocally(event); + publishRemotely(event); + } + + /** + * Publishes an event to this Autopsy node only. + * + * @param event The event to publish. + */ + public void publishLocally(AutopsyEvent event) { + localPublisher.publish(event); + } + + /** + * Publishes an event to other Autopsy nodes only. + * + * @param event The event to publish. + */ + public void publishRemotely(AutopsyEvent event) { + if (null != remotePublisher) { + try { + remotePublisher.publish(event); + } catch (JMSException ex) { + logger.log(Level.SEVERE, String.format("Failed to publish %s event remotely", event.getPropertyName()), ex); //NON-NLS + } + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/events/Bundle.properties b/Core/src/org/sleuthkit/autopsy/events/Bundle.properties new file mode 100755 index 0000000000..4c869740bd --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/events/Bundle.properties @@ -0,0 +1,10 @@ +MessageServiceConnectionInfo.ConnectionCheck.Everything=Invalid hostname, port number, username, and/or password. +MessageServiceConnectionInfo.ConnectionCheck.Hostname=Invalid hostname. +MessageServiceConnectionInfo.ConnectionCheck.Port=Invalid port number. +MessageServiceConnectionInfo.ConnectionCheck.Username=Invalid username. +MessageServiceConnectionInfo.ConnectionCheck.Password=Invalid password. +MessageServiceConnectionInfo.ConnectionCheck.UsernameAndPassword=Invalid username and/or password. +MessageServiceConnectionInfo.ConnectionCheck.HostnameOrPort=Invalid hostname and/or port number. +MessageServiceConnectionInfo.MissingHostname=Missing hostname. +MessageServiceConnectionInfo.MissingUsername=Missing username. +MessageServiceConnectionInfo.MissingPassword=Missing password. \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/events/LocalEventPublisher.java b/Core/src/org/sleuthkit/autopsy/events/LocalEventPublisher.java new file mode 100644 index 0000000000..3972bed895 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/events/LocalEventPublisher.java @@ -0,0 +1,116 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-2015 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.events; + +import java.beans.PropertyChangeListener; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import org.sleuthkit.autopsy.coreutils.Logger; + +/** + * Provides thread-safe support for publishing events to registered subscribers + * on this Autopsy node. Subscribers are constrained to be + * PropertyChangeListeners to integrate with the legacy use of JavaBeans + * PropertyChangeEvents and PropertyChangeListeners as an application event + * system. + */ +final class LocalEventPublisher { + + private static final Logger logger = Logger.getLogger(LocalEventPublisher.class.getName()); + private final Map> subscribersByEvent; + + /** + * Constructs an object for publishing events to registered subscribers on + * this Autopsy node. + */ + LocalEventPublisher() { + subscribersByEvent = new ConcurrentHashMap<>(); + } + + /** + * Adds an event subscriber to this publisher. + * + * @param eventNames The events the subscriber is interested in. + * @param subscriber The subscriber to add. + */ + void addSubscriber(Set eventNames, PropertyChangeListener subscriber) { + for (String eventName : eventNames) { + addSubscriber(eventName, subscriber); + } + } + + /** + * Adds an event subscriber to this publisher. + * + * @param eventName The event the subscriber is interested in. + * @param subscriber The subscriber to add. + */ + void addSubscriber(String eventName, PropertyChangeListener subscriber) { + subscribersByEvent.putIfAbsent(eventName, ConcurrentHashMap.newKeySet()); + Set subscribers = subscribersByEvent.get(eventName); + subscribers.add(subscriber); + } + + /** + * Removes an event subscriber from this publisher. + * + * @param eventNames The events the subscriber is no longer interested in. + * @param subscriber The subscriber to remove. + */ + void removeSubscriber(Set eventNames, PropertyChangeListener subscriber) { + for (String eventName : eventNames) { + removeSubscriber(eventName, subscriber); + } + } + + /** + * Removes an event subscriber from this publisher. + * + * @param eventNames The event the subscriber is no longer interested in. + * @param subscriber The subscriber to remove. + */ + void removeSubscriber(String eventName, PropertyChangeListener subscriber) { + Set subscribers = subscribersByEvent.getOrDefault(eventName, null); + if (null != subscribers) { + subscribers.remove(subscriber); + } + } + + /** + * Publishes an event to all registered subscribers, even if a subscriber + * throws an exception. + * + * @param event The event to be published. + */ + void publish(AutopsyEvent event) { + Set subscribers = subscribersByEvent.getOrDefault(event.getPropertyName(), null); + if (null != subscribers) { + for (PropertyChangeListener subscriber : subscribers) { + try { + subscriber.propertyChange(event); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Exception thrown by subscriber", ex); + } + } + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/events/MessageServiceConnectionInfo.java b/Core/src/org/sleuthkit/autopsy/events/MessageServiceConnectionInfo.java new file mode 100644 index 0000000000..95b40963c0 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/events/MessageServiceConnectionInfo.java @@ -0,0 +1,178 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.events; + +import java.net.URI; +import java.net.URISyntaxException; +import javax.annotation.concurrent.Immutable; +import javax.jms.Connection; +import javax.jms.JMSException; +import org.apache.activemq.ActiveMQConnectionFactory; +import java.io.IOException; +import java.net.InetAddress; +import java.util.MissingResourceException; +import org.openide.util.NbBundle; + +/** + * Connection info for a Java Message Service (JMS) provider. Thread-safe. + */ +@Immutable +public final class MessageServiceConnectionInfo { + + private static final String MESSAGE_SERVICE_URI = "tcp://%s:%s?wireFormat.maxInactivityDuration=0"; + private static final String CONNECTION_TIMED_OUT = "connection timed out"; + private static final String CONNECTION_REFUSED = "connection refused"; + private static final String PASSWORD_OR_USERNAME_BAD = "user name ["; + private static final int IS_REACHABLE_TIMEOUT_MS = 1000; + private final String userName; + private final String password; + private final String host; + private final int port; + + /** + * Constructs an object containing connection info for a Java Message + * Service (JMS) provider. + * + * @param host The host to use for a message service connection. May be + * a host name or an IP address. + * @param port The port number to use for a message service connection. + * @param userName The user name to use for a message service connection. + * @param password The password to use for a message service connection. + * + */ + public MessageServiceConnectionInfo(String host, int port, String userName, String password) { + this.host = host; + this.port = port; + this.userName = userName; + this.password = password; + } + + /** + * Gets the user name to use for a message service connection. + * + * @return The user name as a string. + */ + public String getUserName() { + return userName; + } + + /** + * Gets the password to use for a message service connection. + * + * @return The password as a string. + */ + public String getPassword() { + return password; + } + + /** + * Gets the host to use for a message service connection. May be a host name + * or an IP address. + * + * @return The host as a string. + */ + public String getHost() { + return host; + } + + /** + * Gets the port number to use for a message service connection. + * + * @return The port as an int. + */ + public int getPort() { + return port; + } + + /** + * Gets the TCP URI to use for a message service connection. + * + * @return The URI. + * + * @throws URISyntaxException if the connection info is not for a valid TCP + * URI. + */ + URI getURI() throws URISyntaxException { + return new URI(String.format(MESSAGE_SERVICE_URI, getHost(), Integer.toString(getPort()))); + } + + /** + * Verifies connection to messaging service. Throws if we cannot communicate + * with messaging service. + * + * When issues occur, it attempts to diagnose them by looking at the + * exception messages, returning the appropriate user-facing text for the + * exception received. This method expects the Exceptions messages to be in + * English and compares against English text. + * + * @throws org.sleuthkit.autopsy.events.MessageServiceException + */ + public void tryConnect() throws MessageServiceException { + if (host == null || host.isEmpty()) { + throw new MessageServiceException(NbBundle.getMessage(MessageServiceConnectionInfo.class, "MessageServiceConnectionInfo.MissingHostname")); //NON-NLS + } else if (userName == null || userName.isEmpty()) { + throw new MessageServiceException(NbBundle.getMessage(MessageServiceConnectionInfo.class, "MessageServiceConnectionInfo.MissingUsername")); //NON-NLS + } else if (password == null || password.isEmpty()) { + throw new MessageServiceException(NbBundle.getMessage(MessageServiceConnectionInfo.class, "MessageServiceConnectionInfo.MissingPassword")); //NON-NLS + } + try { + ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(getUserName(), getPassword(), getURI()); + Connection connection = connectionFactory.createConnection(getUserName(), getPassword()); + connection.start(); + connection.close(); + } catch (URISyntaxException ex) { + // The hostname or port seems bad + throw new MessageServiceException(NbBundle.getMessage(MessageServiceConnectionInfo.class, "MessageServiceConnectionInfo.ConnectionCheck.HostnameOrPort")); //NON-NLS + } catch (JMSException ex) { + String result; + Throwable cause = ex.getCause(); + if (cause != null) { + // there is more information from another exception + String msg = cause.getMessage(); + if (msg.startsWith(CONNECTION_TIMED_OUT)) { + // The hostname or IP address seems bad + result = NbBundle.getMessage(MessageServiceConnectionInfo.class, "MessageServiceConnectionInfo.ConnectionCheck.Hostname"); //NON-NLS + } else if (msg.toLowerCase().startsWith(CONNECTION_REFUSED)) { + // The port seems bad + result = NbBundle.getMessage(MessageServiceConnectionInfo.class, "MessageServiceConnectionInfo.ConnectionCheck.Port"); //NON-NLS + } else if (msg.toLowerCase().startsWith(PASSWORD_OR_USERNAME_BAD)) { + // The username or password seems bad + result = NbBundle.getMessage(MessageServiceConnectionInfo.class, "MessageServiceConnectionInfo.ConnectionCheck.UsernameAndPassword"); //NON-NLS + } else { + // Could be either hostname or port number + result = NbBundle.getMessage(MessageServiceConnectionInfo.class, "MessageServiceConnectionInfo.ConnectionCheck.HostnameOrPort"); //NON-NLS + } + } else { + // there is no more information from another exception + try { + if (InetAddress.getByName(getHost()).isReachable(IS_REACHABLE_TIMEOUT_MS)) { + // if we can reach the host, then it's probably a port problem + result = NbBundle.getMessage(MessageServiceConnectionInfo.class, "MessageServiceConnectionInfo.ConnectionCheck.Port"); //NON-NLS + } else { + result = NbBundle.getMessage(MessageServiceConnectionInfo.class, "MessageServiceConnectionInfo.ConnectionCheck.Hostname"); //NON-NLS + } + } catch (IOException | MissingResourceException any) { + // it may be anything + result = NbBundle.getMessage(MessageServiceConnectionInfo.class, "MessageServiceConnectionInfo.ConnectionCheck.Everything"); //NON-NLS + } + } + throw new MessageServiceException(result); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/events/TagDeletedEvent.java b/Core/src/org/sleuthkit/autopsy/events/MessageServiceException.java old mode 100644 new mode 100755 similarity index 50% rename from Core/src/org/sleuthkit/autopsy/events/TagDeletedEvent.java rename to Core/src/org/sleuthkit/autopsy/events/MessageServiceException.java index 6a0a1c5668..f6079cb08d --- a/Core/src/org/sleuthkit/autopsy/events/TagDeletedEvent.java +++ b/Core/src/org/sleuthkit/autopsy/events/MessageServiceException.java @@ -16,30 +16,38 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.sleuthkit.autopsy.events; -import javax.annotation.concurrent.Immutable; -import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.datamodel.Tag; - /** - * Base Class for events that are fired when a Tag is deleted + * Provides a system exception for the Message Service. */ -@Immutable -abstract class TagDeletedEvent extends TagEvent { +public class MessageServiceException extends Exception { + private static final long serialVersionUID = 1L; - protected TagDeletedEvent(String propertyName, T oldValue) { - super(Case.class, propertyName, oldValue, null); + /** + * Constructs a new exception with null as its message. + */ + public MessageServiceException() { + super(); } /** - * get the Tag that was deleted + * Constructs a new exception with the specified message. * - * @return the Tag + * @param message The message. */ - @SuppressWarnings("unchecked") - @Override - public T getTag() { - return (T) getOldValue(); + public MessageServiceException(String message) { + super(message); + } + + /** + * Constructs a new exception with the specified message and cause. + * + * @param message The message. + * @param cause The cause. + */ + public MessageServiceException(String message, Throwable cause) { + super(message, cause); } } diff --git a/Core/src/org/sleuthkit/autopsy/events/RemoteEventPublisher.java b/Core/src/org/sleuthkit/autopsy/events/RemoteEventPublisher.java new file mode 100644 index 0000000000..58a567e415 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/events/RemoteEventPublisher.java @@ -0,0 +1,158 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-2015 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.events; + +import java.net.URISyntaxException; +import java.util.logging.Level; +import javax.jms.Connection; +import javax.jms.DeliveryMode; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageConsumer; +import javax.jms.MessageListener; +import javax.jms.MessageProducer; +import javax.jms.ObjectMessage; +import javax.jms.Session; +import javax.jms.Topic; +import org.apache.activemq.ActiveMQConnectionFactory; +import org.sleuthkit.autopsy.coreutils.Logger; + +/** + * Provides thread-safe support for publishing events to registered subscribers + * on other Autopsy nodes and for publishing events from other Autopsy nodes. + * Subscribers on this node are constrained to be PropertyChangeListeners to + * integrate with the legacy use of JavaBeans PropertyChangeEvents and + * PropertyChangeListeners as an application event system. + */ +final class RemoteEventPublisher { + + private static final Logger logger = Logger.getLogger(RemoteEventPublisher.class.getName()); + private static final String ALL_MESSAGE_SELECTOR = "All"; + private final LocalEventPublisher localPublisher; + private final Connection connection; + private final Session session; + private final MessageProducer producer; + private final MessageConsumer consumer; + private final MessageReceiver receiver; + + /** + * Constructs an object for publishing events to registered subscribers on + * other Autopsy nodes and for publishing events from other Autopsy nodes. + * + * @param eventChannelName The name of the event channel to be used for + * communication with other Autopsy nodes. + * @param localPublisher An event publisher that will be used to publish + * events from other Autopsy nodes on this node. + * @param info Connection info for the message service. + * + * @throws URISyntaxException if the URI in the connection info is + * malformed. + * @throws JMSException if the connection to the message service + * cannot be made. + */ + RemoteEventPublisher(String eventChannelName, LocalEventPublisher localPublisher, MessageServiceConnectionInfo info) throws URISyntaxException, JMSException { + try { + this.localPublisher = localPublisher; + ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(info.getUserName(), info.getPassword(), info.getURI()); + connection = connectionFactory.createConnection(); + connection.start(); + session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + Topic topic = session.createTopic(eventChannelName); + producer = session.createProducer(topic); + producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT); + consumer = session.createConsumer(topic, "events = '" + ALL_MESSAGE_SELECTOR + "'", true); + receiver = new MessageReceiver(); + consumer.setMessageListener(receiver); + } catch (URISyntaxException | JMSException ex) { + logger.log(Level.SEVERE, "Failed to connect to event channel", ex); + try { + stop(); + } catch (JMSException ignored) { + /** + * Not surprising if there is some error here, but it was worth + * trying to clean up. + */ + } + throw ex; + } + } + + /** + * Stops this publisher, causing it to disconnect from the message service. + * + * @throws JMSException if there is a problem closing the session or the + * connection. + */ + synchronized void stop() throws JMSException { + if (null != producer) { + producer.close(); + } + if (null != consumer) { + consumer.close(); + } + if (null != session) { + session.close(); + } + if (null != connection) { + connection.close(); + } + } + + /** + * Sends an event message to the message service. + * + * @param event The event to publish. + */ + synchronized void publish(AutopsyEvent event) throws JMSException { + ObjectMessage message = session.createObjectMessage(); + message.setStringProperty("events", ALL_MESSAGE_SELECTOR); + message.setObject(event); + producer.send(message); + } + + /** + * Receives event messages from the message service and publishes them + * locally. + */ + private final class MessageReceiver implements MessageListener { + + /** + * Receives an event message from the message service and publishes it + * locally. Called by a JMS thread. + * + * @param message The message. + */ + @Override + public void onMessage(Message message) { + try { + if (message instanceof ObjectMessage) { + ObjectMessage objectMessage = (ObjectMessage) message; + Object object = objectMessage.getObject(); + if (object instanceof AutopsyEvent) { + AutopsyEvent event = (AutopsyEvent) object; + event.setSourceType(AutopsyEvent.SourceType.REMOTE); + localPublisher.publish(event); + } + } + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error receiving message", ex); + } + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/events/TagEvent.java b/Core/src/org/sleuthkit/autopsy/events/TagEvent.java deleted file mode 100644 index b80725782d..0000000000 --- a/Core/src/org/sleuthkit/autopsy/events/TagEvent.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ -package org.sleuthkit.autopsy.events; - -import org.sleuthkit.datamodel.Tag; - -/** - * - */ -abstract public class TagEvent extends AutopsyEvent { - - public TagEvent(Object source, String propertyName, Object oldValue, Object newValue) { - super(source, propertyName, oldValue, newValue); - } - - /** - * get the Tag that this event is for - * - * @return the Tag - */ - public abstract T getTag(); - -} diff --git a/Core/src/org/sleuthkit/autopsy/examples/SampleExecutableDataSourceIngestModule.java b/Core/src/org/sleuthkit/autopsy/examples/SampleExecutableDataSourceIngestModule.java index b65de71c98..b76bd08bc7 100755 --- a/Core/src/org/sleuthkit/autopsy/examples/SampleExecutableDataSourceIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/examples/SampleExecutableDataSourceIngestModule.java @@ -84,7 +84,7 @@ public class SampleExecutableDataSourceIngestModule implements DataSourceIngestM this.context = context; if (refCounter.incrementAndGet(context.getJobId()) == 1) { // Create an output directory for this job. - outputDirPath = Case.getCurrentCase().getModulesOutputDirAbsPath() + File.separator + moduleName; //NON-NLS + outputDirPath = Case.getCurrentCase().getModuleDirectory() + File.separator + moduleName; //NON-NLS File outputDir = new File(outputDirPath); if (outputDir.exists() == false) { outputDir.mkdirs(); diff --git a/Core/src/org/sleuthkit/autopsy/filesearch/Bundle.properties b/Core/src/org/sleuthkit/autopsy/filesearch/Bundle.properties index ea78d740c9..99c61a24ca 100644 --- a/Core/src/org/sleuthkit/autopsy/filesearch/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/filesearch/Bundle.properties @@ -47,6 +47,7 @@ FileSearchPanel.search.results.msg=File Search\: {0} matches found FileSearchPanel.search.results.details=Large number of matches may impact performance on some operations FileSearchPanel.search.exception.noFilterSelected.msg=At least one filter must be selected. FileSearchPanel.search.validationErr.msg=Validation Error\: {0} +FileSearchPanel.emptyWhereClause.text=Invalid options, nothing to show. KnownStatusSearchFilter.noneSelectedMsg.text=At least one known status must be selected\! NameSearchFilter.emptyNameMsg.text=Must enter something for name search. SearchNode.getName.text=Search Result diff --git a/Core/src/org/sleuthkit/autopsy/filesearch/DateSearchFilter.java b/Core/src/org/sleuthkit/autopsy/filesearch/DateSearchFilter.java index cc6258848d..be8a799556 100644 --- a/Core/src/org/sleuthkit/autopsy/filesearch/DateSearchFilter.java +++ b/Core/src/org/sleuthkit/autopsy/filesearch/DateSearchFilter.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011 Basis Technology Corp. + * Copyright 2011-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,8 +37,8 @@ import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JSeparator; import javax.swing.ListCellRenderer; +import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; - import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; @@ -72,7 +72,7 @@ class DateSearchFilter extends AbstractFileSearchFilter { @Override public String getPredicate() throws FilterValidationException { - String addQuery = "1"; + String query = "NULL"; DateSearchPanel panel = this.getComponent(); // first, get the selected timeZone from the dropdown list @@ -92,7 +92,7 @@ class DateSearchFilter extends AbstractFileSearchFilter { startDate = Calendar.getInstance(new SimpleTimeZone(0, "GMT")); //NON-NLS startDate.setTime(temp); // convert to GMT } catch (ParseException ex) { - // for now, no need to show the error message to the user her + // for now, no need to show the error message to the user here } if (!startDateValue.equals("")) { if (startDate != null) { @@ -120,6 +120,13 @@ class DateSearchFilter extends AbstractFileSearchFilter { } } + // If they put the dates in backwards, help them out. + if (fromDate > toDate) { + long temp = toDate; + toDate = fromDate; + fromDate = temp; + } + final boolean modifiedChecked = panel.getModifiedCheckBox().isSelected(); final boolean changedChecked = panel.getChangedCheckBox().isSelected(); final boolean accessedChecked = panel.getAccessedCheckBox().isSelected(); @@ -127,30 +134,27 @@ class DateSearchFilter extends AbstractFileSearchFilter { if (modifiedChecked || changedChecked || accessedChecked || createdChecked) { - String subQuery = "0"; - if (modifiedChecked) { - subQuery += " or mtime between " + fromDate + " and " + toDate; //NON-NLS + query += " OR (mtime BETWEEN " + fromDate + " AND " + toDate + ")"; //NON-NLS } if (changedChecked) { - subQuery += " or ctime between " + fromDate + " and " + toDate; //NON-NLS + query += " OR (ctime BETWEEN " + fromDate + " AND " + toDate + ")"; //NON-NLS } if (accessedChecked) { - subQuery += " or atime between " + fromDate + " and " + toDate; //NON-NLS + query += " OR (atime BETWEEN " + fromDate + " AND " + toDate + ")"; //NON-NLS } if (createdChecked) { - subQuery += " or crtime between " + fromDate + " and " + toDate; //NON-NLS + query += " OR (crtime BETWEEN " + fromDate + " AND " + toDate + ")"; //NON-NLS } - addQuery += " and (" + subQuery + ")"; //NON-NLS } else { throw new FilterValidationException(NONE_SELECTED_MESSAGE); } - return addQuery; + return query; } @@ -160,7 +164,7 @@ class DateSearchFilter extends AbstractFileSearchFilter { private static List createTimeZoneList() { - List timeZones = new ArrayList(); + List timeZones = new ArrayList<>(); if (Case.existsCurrentCase()) { // get the latest case @@ -237,25 +241,33 @@ class DateSearchFilter extends AbstractFileSearchFilter { @Override public void propertyChange(PropertyChangeEvent evt) { - String changed = evt.getPropertyName(); - Object oldValue = evt.getOldValue(); - Object newValue = evt.getNewValue(); - - if (changed.equals(Case.Events.CURRENT_CASE.toString().toString())) { - // create or open a case - if (newValue != null) { - DateSearchFilter.this.updateTimeZoneList(); - } - } - - // if the image is added to the case - if (changed.equals(Case.Events.DATA_SOURCE_ADDED.toString())) { - DateSearchFilter.this.updateTimeZoneList(); - } - - // if the image is removed from the case - if (changed.equals(Case.Events.DATA_SOURCE_DELETED.toString())) { - DateSearchFilter.this.updateTimeZoneList(); + switch (Case.Events.valueOf(evt.getPropertyName())) { + case CURRENT_CASE: + Object newValue = evt.getNewValue(); + if (null != newValue) { + /** + * Opening a new case. + */ + SwingUtilities.invokeLater(DateSearchFilter.this::updateTimeZoneList); + } + break; + case DATA_SOURCE_ADDED: + case DATA_SOURCE_DELETED: + /** + * Checking for a current case is a stop gap measure until a + * different way of handling the closing of cases is worked + * out. Currently, remote events may be received for a case + * that is already closed. + */ + try { + Case.getCurrentCase(); + SwingUtilities.invokeLater(DateSearchFilter.this::updateTimeZoneList); + } catch (IllegalStateException notUsed) { + /** + * Case is closed, do nothing. + */ + } + break; } } } diff --git a/Core/src/org/sleuthkit/autopsy/filesearch/FileSearchDialog.java b/Core/src/org/sleuthkit/autopsy/filesearch/FileSearchDialog.java index a7d7bf56a7..66cca80df1 100644 --- a/Core/src/org/sleuthkit/autopsy/filesearch/FileSearchDialog.java +++ b/Core/src/org/sleuthkit/autopsy/filesearch/FileSearchDialog.java @@ -31,6 +31,7 @@ import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JFrame; +import org.openide.windows.WindowManager; /** * File search dialog @@ -50,6 +51,7 @@ class FileSearchDialog extends javax.swing.JDialog { double w = getSize().getWidth(); double h = getSize().getHeight(); setLocation((int) ((screenDimension.getWidth() - w) / 2), (int) ((screenDimension.getHeight() - h) / 2)); + this.setLocationRelativeTo(WindowManager.getDefault().getMainWindow()); fileSearchPanel1.addListenerToAll(new ActionListener() { @Override diff --git a/Core/src/org/sleuthkit/autopsy/filesearch/FileSearchPanel.java b/Core/src/org/sleuthkit/autopsy/filesearch/FileSearchPanel.java index 9901655fc6..a8ee427c24 100644 --- a/Core/src/org/sleuthkit/autopsy/filesearch/FileSearchPanel.java +++ b/Core/src/org/sleuthkit/autopsy/filesearch/FileSearchPanel.java @@ -63,6 +63,7 @@ class FileSearchPanel extends javax.swing.JPanel { private List filterAreas = new ArrayList(); private JButton searchButton; private static int resultWindowCount = 0; //keep track of result windows so they get unique names + private static final String EMPTY_WHERE_CLAUSE = NbBundle.getMessage(DateSearchFilter.class, "FileSearchPanel.emptyWhereClause.text"); /** * Creates new form FileSearchPanel @@ -151,7 +152,6 @@ class FileSearchPanel extends javax.swing.JPanel { List contentList = null; try { SleuthkitCase tskDb = currentCase.getSleuthkitCase(); - //ResultSet rs = tempDb.runQuery(this.getQuery("count(*) as TotalMatches")); contentList = tskDb.findAllFilesWhere(this.getQuery()); } catch (TskCoreException ex) { @@ -212,13 +212,24 @@ class FileSearchPanel extends javax.swing.JPanel { */ private String getQuery() throws FilterValidationException { - //String query = "select " + tempQuery + " from tsk_files where 1"; - String query = " 1"; - + //String query = "SELECT " + tempQuery + " FROM tsk_files WHERE "; + String query = ""; + int i = 0; for (FileSearchFilter f : this.getEnabledFilters()) { - query += " and (" + f.getPredicate() + ")"; //NON-NLS + String result = f.getPredicate(); + if (!result.isEmpty()) { + if (i > 0) { + query += " AND (" + result + ")"; //NON-NLS + } else { + query += " (" + result + ")"; //NON-NLS + } + ++i; + } } + if (query.isEmpty()) { + throw new FilterValidationException(EMPTY_WHERE_CLAUSE); + } return query; } diff --git a/Core/src/org/sleuthkit/autopsy/filesearch/KnownStatusSearchFilter.java b/Core/src/org/sleuthkit/autopsy/filesearch/KnownStatusSearchFilter.java index 866555092a..bd9776f61d 100644 --- a/Core/src/org/sleuthkit/autopsy/filesearch/KnownStatusSearchFilter.java +++ b/Core/src/org/sleuthkit/autopsy/filesearch/KnownStatusSearchFilter.java @@ -56,15 +56,15 @@ class KnownStatusSearchFilter extends AbstractFileSearchFilter\u30ED\u30B0\u30D5\u30A9\u30EB\u30C0\u30FC\u3092\u958B\u304F\uFF09 -IngestManager.IngestMessage.ErrorMessageLimitReached.subject=\u6700\u5927\u6570\u306E\u30A8\u30E9\u30FC\u304C\u63B2\u8F09\u3055\u308C\u307E\u3057\u305F -IngestManager.IngestMessage.ErrorMessageLimitReached.title=\u30A4\u30F3\u30B8\u30A7\u30B9\u30C8\u30DE\u30CD\u30B8\u30E3\u30FC -IngestManager.IngestThreadActivitySnapshot.idleThread=\u30A2\u30A4\u30C9\u30EB -IngestProgressSnapshotDialog.title.text=\u30A4\u30F3\u30B8\u30A7\u30B9\u30C8\u30D7\u30ED\u30B0\u30EC\u30B9\u30FB\u30B9\u30CA\u30C3\u30D7\u30B7\u30E7\u30C3\u30C8 -IngestProgressSnapshotPanel.closeButton.text=\u9589\u3058\u308B -IngestProgressSnapshotPanel.refreshButton.text=\u30EA\u30D5\u30EC\u30C3\u30B7\u30E5 -IngestProgressSnapshotPanel.SnapshotsTableModel.colNames.activity=\u30A2\u30AF\u30C6\u30A3\u30D3\u30C6\u30A3 -IngestJobSettingsPanel.advancedButton.actionCommand=\u30A2\u30C9\u30D0\u30F3\u30B9 -IngestJobSettingsPanel.advancedButton.text=\u30A2\u30C9\u30D0\u30F3\u30B9 -IngestJobSettingsPanel.processUnallocCheckbox.toolTipText=\u524A\u9664\u3055\u308C\u305F\u30D5\u30A1\u30A4\u30EB\u7B49\u306E\u672A\u5272\u308A\u5F53\u3066\u9818\u57DF\u3092\u51E6\u7406\u3002\u3088\u308A\u5B8C\u5168\u306A\u7D50\u679C\u304C\u51FA\u307E\u3059\u304C\u3001\u5927\u304D\u3044\u30A4\u30E1\u30FC\u30B8\u3067\u306F\u51E6\u7406\u6642\u9593\u304C\u9577\u304F\u306A\u308B\u304B\u3082\u3057\u308C\u307E\u305B\u3093\u3002 -IngestJobSettingsPanel.processUnallocCheckbox.text=\u672A\u5272\u308A\u5F53\u3066\u9818\u57DF\u306E\u51E6\u7406 +IngestMessageTopComponent.initComponents.name=\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u30a4\u30f3\u30dc\u30c3\u30af\u30b9 +IngestMessageTopComponent.msgDlg.ingestRpt.text=\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u30ec\u30dd\u30fc\u30c8 +IngestMonitor.mgrErrMsg.lowDiskSpace.msg=\u30c7\u30a3\u30b9\u30af{0}\u306e\u30c7\u30a3\u30b9\u30af\u9818\u57df\u4e0d\u8db3\u306e\u305f\u3081\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u3092\u4e2d\u6b62\u3057\u307e\u3059\u3002\n\u30b1\u30fc\u30b9\u30c9\u30e9\u30a4\u30d6\u306b\u6700\u4f4e1GB\u306e\u7a7a\u304d\u9818\u57df\u304c\u3042\u308b\u306e\u3092\u78ba\u8a8d\u3057\u3001\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u3092\u518d\u30b9\u30bf\u30fc\u30c8\u3057\u3066\u4e0b\u3055\u3044\u3002 +IngestMonitor.mgrErrMsg.lowDiskSpace.title=\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u304c\u4e2d\u6b62\u3055\u308c\u307e\u3057\u305f\u30fc{0}\u306e\u30c7\u30a3\u30b9\u30af\u9818\u57df\u4e0d\u8db3 +OpenIDE-Module-Name=\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8 +IngestManager.StartIngestJobsTask.run.startupErr.dlgErrorList=\n\u30a8\u30e9\u30fc\uff1a\n{0} +IngestManager.StartIngestJobsTask.run.startupErr.dlgMsg=\uff11\u3064\u307e\u305f\u306f\u8907\u6570\u306e\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u30e2\u30b8\u30e5\u30fc\u30eb\u3092\u30b9\u30bf\u30fc\u30c8\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u30b8\u30e7\u30d6\u306f\u30ad\u30e3\u30f3\u30bb\u30eb\u3055\u308c\u307e\u3057\u305f\u3002 +IngestManager.StartIngestJobsTask.run.startupErr.dlgSolution=\u5931\u6557\u3057\u305f\u30e2\u30b8\u30e5\u30fc\u30eb\u3092\u7121\u52b9\u5316\u3059\u308b\u304b\u30a8\u30e9\u30fc\u3092\u89e3\u6c7a\u3057\u3001\u305d\u306e\u5f8c\u30c7\u30fc\u30bf\u30bd\u30fc\u30b9\u3092\u53f3\u30af\u30ea\u30c3\u30af\u3057\u3001\n\u300c\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u30e2\u30b8\u30e5\u30fc\u30eb\u5b9f\u884c\u300d\u3092\u9078\u629e\u3057\u3066\u3001\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u3092\u30ea\u30b9\u30bf\u30fc\u30c8\u3057\u3066\u4e0b\u3055\u3044\u3002 +IngestManager.StartIngestJobsTask.run.startupErr.dlgTitle=\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u5931\u6557 +IngestJobSettings.createModuleSettingsFolder.warning=\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u30e2\u30b8\u30e5\u30fc\u30eb\u8a2d\u5b9a\u30d5\u30a9\u30eb\u30c0\u306e\u4f5c\u6210\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u8a2d\u5b9a\u3092\u4fdd\u5b58\u3067\u304d\u307e\u305b\u3093\u3002 +IngestJob.progress.dataSourceIngest.initialDisplayName={0}\u3092\u89e3\u6790\u4e2d +IngestProgressSnapshotPanel.SnapshotsTableModel.colNames.dataSource=\u30c7\u30fc\u30bf\u30bd\u30fc\u30b9 +IngestProgressSnapshotPanel.SnapshotsTableModel.colNames.elapsedTime=\u7d4c\u904e\u6642\u9593\uff08\u6642\uff1a\u5206\uff1a\u79d2\uff09 +IngestProgressSnapshotPanel.SnapshotsTableModel.colNames.file=\u30d5\u30a1\u30a4\u30eb +IngestProgressSnapshotPanel.SnapshotsTableModel.colNames.startTime=\u958b\u59cb\u6642\u9593 +IngestProgressSnapshotPanel.SnapshotsTableModel.colNames.threadID=\u30b9\u30ec\u30c3\u30c9ID +IngestManager.IngestMessage.ErrorMessageLimitReached.msg=\u6700\u5927\u6570({0})\u306e\u30a8\u30e9\u30fc\u304a\u3088\u3073\u307e\u305f\u306f\u8b66\u544a\u30e1\u30c3\u30bb\u30fc\u30b8\u304c\u63b2\u8f09\u3055\u308c\u307e\u3057\u305f\u3002\u3055\u3089\u306a\u308b\u30a8\u30e9\u30fc\uff0f\u8b66\u544a\u306f\u30ed\u30b0\u3092\u78ba\u8a8d\u3057\u3066\u4e0b\u3055\u3044\uff08\u30d8\u30eb\u30d7->\u30ed\u30b0\u30d5\u30a9\u30eb\u30c0\u30fc\u3092\u958b\u304f\uff09 +IngestManager.IngestMessage.ErrorMessageLimitReached.subject=\u6700\u5927\u6570\u306e\u30a8\u30e9\u30fc\u304c\u63b2\u8f09\u3055\u308c\u307e\u3057\u305f +IngestManager.IngestMessage.ErrorMessageLimitReached.title=\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u30de\u30cd\u30b8\u30e3\u30fc +IngestManager.IngestThreadActivitySnapshot.idleThread=\u30a2\u30a4\u30c9\u30eb +IngestProgressSnapshotDialog.title.text=\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u30d7\u30ed\u30b0\u30ec\u30b9\u30fb\u30b9\u30ca\u30c3\u30d7\u30b7\u30e7\u30c3\u30c8 +IngestProgressSnapshotPanel.closeButton.text=\u9589\u3058\u308b +IngestProgressSnapshotPanel.refreshButton.text=\u30ea\u30d5\u30ec\u30c3\u30b7\u30e5 +IngestProgressSnapshotPanel.SnapshotsTableModel.colNames.activity=\u30a2\u30af\u30c6\u30a3\u30d3\u30c6\u30a3 +IngestJobSettingsPanel.advancedButton.actionCommand=\u30a2\u30c9\u30d0\u30f3\u30b9 +IngestJobSettingsPanel.advancedButton.text=\u30a2\u30c9\u30d0\u30f3\u30b9 ModuleTableModel.colName.duration=\u6240\u8981\u6642\u9593 -IngestJob.cancellationDialog.title=\u30A4\u30F3\u30B8\u30A7\u30B9\u30C8\u3092\u30AD\u30E3\u30F3\u30BB\u30EB +IngestJob.cancellationDialog.title=\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u3092\u30ad\u30e3\u30f3\u30bb\u30eb IngestMessagesToolbar.toolTipText= IngestMessageDetailsPanel.messageDetailsPane.toolTipText= IngestMessageDetailsPanel.backButton.text= -IngestJobSettings.missingModule.warning=\u4EE5\u524D\u306B\u8AAD\u307F\u8FBC\u3093\u3060{0}\u30E2\u30B8\u30E5\u30FC\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F\u3002 -DataSourceIngestCancellationPanel.cancelAllModulesRadioButton.text=\u5168\u3066\u306E\u30A4\u30F3\u30B8\u30A7\u30B9\u30C8\u30E2\u30B8\u30E5\u30FC\u30EB\u3092\u30AD\u30E3\u30F3\u30BB\u30EB -DataSourceIngestCancellationPanel.cancelCurrentModuleRadioButton.text=\u73FE\u5728\u306E\u30A4\u30F3\u30B8\u30A7\u30B9\u30C8\u30E2\u30B8\u30E5\u30FC\u30EB\u306E\u307F\u30AD\u30E3\u30F3\u30BB\u30EB -FileIngestCancellationPanel.cancelFileIngestRadioButton.text=\u30D5\u30A1\u30A4\u30EB\u30A4\u30F3\u30B8\u30A7\u30B9\u30C8\u306E\u307F\u30AD\u30E3\u30F3\u30BB\u30EB -FileIngestCancellationPanel.cancelIngestJobRadioButton.text=\u30C7\u30FC\u30BF\u30BD\u30FC\u30B9\u30A4\u30F3\u30B8\u30A7\u30B9\u30C8\u304A\u3088\u3073\u30D5\u30A1\u30A4\u30EB\u30A4\u30F3\u30B8\u30A7\u30B9\u30C8\u3092\u30AD\u30E3\u30F3\u30BB\u30EB -IngestJobSettings.moduleSettingsLoad.warning=\u30C7\u30D5\u30A9\u30EB\u30C8\u3092\u4F7F\u7528\u3057\u3001{1}\u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\u306E{0}\u30E2\u30B8\u30E5\u30FC\u30EB\u7528\u306E\u30A4\u30F3\u30B8\u30A7\u30B9\u30C8\u30B8\u30E7\u30D6\u8A2D\u5B9A\u3092\u8AAD\u307F\u8FBC\u307F\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002\ -IngestJobSettings.moduleSettingsSave.warning={1}\u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\u306E{0}\u30E2\u30B8\u30E5\u30FC\u30EB\u7528\u306E\u30A4\u30F3\u30B8\u30A7\u30B9\u30C8\u30B8\u30E7\u30D6\u8A2D\u5B9A\u3092\u4FDD\u5B58\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002 -IngestJobSettings.save.warning={0}\u30E2\u30B8\u30E5\u30FC\u30EB\u306E\u30A4\u30F3\u30B8\u30A7\u30B9\u30C8\u30B8\u30E7\u30D6\u8A2D\u5B9A\u3092\u4FDD\u5B58\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002 -IngestJobTableModel.colName.dataSource=\u30C7\u30FC\u30BF\u30BD\u30FC\u30B9 -IngestJobTableModel.colName.dirQueued=\u30AD\u30E5\u30FC\u3055\u308C\u305F\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA -IngestJobTableModel.colName.filesPerSec=\u30D5\u30A1\u30A4\u30EB\uFF0F\u79D2 -IngestJobTableModel.colName.filesQueued=\u30AD\u30E5\u30FC\u3055\u308C\u305F\u30D5\u30A1\u30A4\u30EB -IngestJobTableModel.colName.inProgress=\u51E6\u7406\u4E2D -IngestJobTableModel.colName.jobID=\u30B8\u30E7\u30D6ID -IngestJobTableModel.colName.numProcessed=\u51E6\u7406\u3055\u308C\u305F\u6570 -IngestJobTableModel.colName.rootQueued=\u30AD\u30E5\u30FC\u3055\u308C\u305F\u30EB\u30FC\u30C8 -IngestJobTableModel.colName.start=\u30B9\u30BF\u30FC\u30C8 -IngestModuleFactoryLoader.errorMessages.duplicateDisplayName=\u5225\u306E\u30E2\u30B8\u30E5\u30FC\u30EB\u306E\u540D\u524D\u3092\u91CD\u8907\u3059\u308B\u3001{0}\u306E\u540D\u524D\u3092\u6301\u3064\u30E2\u30B8\u30E5\u30FC\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u3057\u305F\u3002\u30E2\u30B8\u30E5\u30FC\u30EB\u306F\u4F7F\u7528\u3057\u307E\u305B\u3093\u3002 -IngestProgressSnapshotPanel.SnapshotsTableModel.colNames.jobID=\u30B8\u30E7\u30D6ID -ModuleTableModel.colName.module=\u30E2\u30B8\u30E5\u30FC\u30EB \ No newline at end of file +IngestJobSettings.missingModule.warning=\u4ee5\u524d\u306b\u8aad\u307f\u8fbc\u3093\u3060{0}\u30e2\u30b8\u30e5\u30fc\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002 +DataSourceIngestCancellationPanel.cancelAllModulesRadioButton.text=\u5168\u3066\u306e\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u30e2\u30b8\u30e5\u30fc\u30eb\u3092\u30ad\u30e3\u30f3\u30bb\u30eb +DataSourceIngestCancellationPanel.cancelCurrentModuleRadioButton.text=\u73fe\u5728\u306e\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u30e2\u30b8\u30e5\u30fc\u30eb\u306e\u307f\u30ad\u30e3\u30f3\u30bb\u30eb +FileIngestCancellationPanel.cancelFileIngestRadioButton.text=\u30d5\u30a1\u30a4\u30eb\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u306e\u307f\u30ad\u30e3\u30f3\u30bb\u30eb +FileIngestCancellationPanel.cancelIngestJobRadioButton.text=\u30c7\u30fc\u30bf\u30bd\u30fc\u30b9\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u304a\u3088\u3073\u30d5\u30a1\u30a4\u30eb\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u3092\u30ad\u30e3\u30f3\u30bb\u30eb +IngestJobSettings.moduleSettingsLoad.warning=\u30c7\u30d5\u30a9\u30eb\u30c8\u3092\u4f7f\u7528\u3057\u3001{1}\u30b3\u30f3\u30c6\u30ad\u30b9\u30c8\u306e{0}\u30e2\u30b8\u30e5\u30fc\u30eb\u7528\u306e\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u30b8\u30e7\u30d6\u8a2d\u5b9a\u3092\u8aad\u307f\u8fbc\u307f\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\ +IngestJobSettings.moduleSettingsSave.warning={1}\u30b3\u30f3\u30c6\u30ad\u30b9\u30c8\u306e{0}\u30e2\u30b8\u30e5\u30fc\u30eb\u7528\u306e\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u30b8\u30e7\u30d6\u8a2d\u5b9a\u3092\u4fdd\u5b58\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002 +IngestJobSettings.save.warning={0}\u30e2\u30b8\u30e5\u30fc\u30eb\u306e\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u30b8\u30e7\u30d6\u8a2d\u5b9a\u3092\u4fdd\u5b58\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002 +IngestJobTableModel.colName.dataSource=\u30c7\u30fc\u30bf\u30bd\u30fc\u30b9 +IngestJobTableModel.colName.dirQueued=\u30ad\u30e5\u30fc\u3055\u308c\u305f\u30c7\u30a3\u30ec\u30af\u30c8\u30ea +IngestJobTableModel.colName.filesPerSec=\u30d5\u30a1\u30a4\u30eb\uff0f\u79d2 +IngestJobTableModel.colName.filesQueued=\u30ad\u30e5\u30fc\u3055\u308c\u305f\u30d5\u30a1\u30a4\u30eb +IngestJobTableModel.colName.inProgress=\u51e6\u7406\u4e2d +IngestJobTableModel.colName.jobID=\u30b8\u30e7\u30d6ID +IngestJobTableModel.colName.numProcessed=\u51e6\u7406\u3055\u308c\u305f\u6570 +IngestJobTableModel.colName.rootQueued=\u30ad\u30e5\u30fc\u3055\u308c\u305f\u30eb\u30fc\u30c8 +IngestJobTableModel.colName.start=\u30b9\u30bf\u30fc\u30c8 +IngestModuleFactoryLoader.errorMessages.duplicateDisplayName=\u5225\u306e\u30e2\u30b8\u30e5\u30fc\u30eb\u306e\u540d\u524d\u3092\u91cd\u8907\u3059\u308b\u3001{0}\u306e\u540d\u524d\u3092\u6301\u3064\u30e2\u30b8\u30e5\u30fc\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u3057\u305f\u3002\u30e2\u30b8\u30e5\u30fc\u30eb\u306f\u4f7f\u7528\u3057\u307e\u305b\u3093\u3002 +IngestProgressSnapshotPanel.SnapshotsTableModel.colNames.jobID=\u30b8\u30e7\u30d6ID +ModuleTableModel.colName.module=\u30e2\u30b8\u30e5\u30fc\u30eb +IngestJobSettingsPanel.processUnallocCheckbox.toolTipText=\u524a\u9664\u3055\u308c\u305f\u30d5\u30a1\u30a4\u30eb\u7b49\u306e\u672a\u5272\u308a\u5f53\u3066\u9818\u57df\u3092\u51e6\u7406\u3002\u3088\u308a\u5b8c\u5168\u306a\u7d50\u679c\u304c\u51fa\u307e\u3059\u304c\u3001\u5927\u304d\u3044\u30a4\u30e1\u30fc\u30b8\u3067\u306f\u51e6\u7406\u6642\u9593\u304c\u9577\u304f\u306a\u308b\u304b\u3082\u3057\u308c\u307e\u305b\u3093\u3002 + +IngestJobSettingsPanel.processUnallocCheckbox.text=\u672a\u5272\u308a\u5f53\u3066\u9818\u57df\u306e\u51e6\u7406 diff --git a/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestJob.java b/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestJob.java index 4fde035ccd..61784161bc 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestJob.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestJob.java @@ -130,7 +130,7 @@ final class DataSourceIngestJob { * A data source ingest job can run interactively using NetBeans progress * handles. */ - private final boolean runInteractively; + private final boolean doUI; /** * A data source ingest job uses these fields to report data source level @@ -170,7 +170,7 @@ final class DataSourceIngestJob { this.id = DataSourceIngestJob.nextJobId.getAndIncrement(); this.dataSource = dataSource; this.settings = settings; - this.runInteractively = runInteractively; + this.doUI = runInteractively; this.createTime = new Date().getTime(); this.createIngestPipelines(); } @@ -386,9 +386,11 @@ final class DataSourceIngestJob { // errors are likely redundant. while (!this.fileIngestPipelinesQueue.isEmpty()) { pipeline = this.fileIngestPipelinesQueue.poll(); - List shutDownErrors = pipeline.shutDown(); - if (!shutDownErrors.isEmpty()) { - logIngestModuleErrors(shutDownErrors); + if (pipeline.isRunning()) { + List shutDownErrors = pipeline.shutDown(); + if (!shutDownErrors.isEmpty()) { + logIngestModuleErrors(shutDownErrors); + } } } break; @@ -411,7 +413,7 @@ final class DataSourceIngestJob { } } - if (this.runInteractively) { + if (this.doUI) { /** * Start one or both of the first stage ingest progress bars. */ @@ -462,7 +464,7 @@ final class DataSourceIngestJob { private void startSecondStage() { logger.log(Level.INFO, "Starting second stage analysis for {0} (jobId={1})", new Object[]{dataSource.getName(), this.id}); this.stage = DataSourceIngestJob.Stages.SECOND; - if (this.runInteractively) { + if (this.doUI) { this.startDataSourceIngestProgressBar(); } synchronized (this.dataSourceIngestPipelineLock) { @@ -476,7 +478,7 @@ final class DataSourceIngestJob { * Starts a data source level ingest progress bar for this job. */ private void startDataSourceIngestProgressBar() { - if (this.runInteractively) { + if (this.doUI) { synchronized (this.dataSourceIngestProgressLock) { String displayName = NbBundle.getMessage(this.getClass(), "IngestJob.progress.dataSourceIngest.initialDisplayName", @@ -511,7 +513,7 @@ final class DataSourceIngestJob { * Starts the file level ingest progress bar for this job. */ private void startFileIngestProgressBar() { - if (this.runInteractively) { + if (this.doUI) { synchronized (this.fileIngestProgressLock) { String displayName = NbBundle.getMessage(this.getClass(), "IngestJob.progress.fileIngest.displayName", @@ -565,13 +567,15 @@ final class DataSourceIngestJob { List errors = new ArrayList<>(); while (!this.fileIngestPipelinesQueue.isEmpty()) { FileIngestPipeline pipeline = fileIngestPipelinesQueue.poll(); - errors.addAll(pipeline.shutDown()); + if (pipeline.isRunning()) { + errors.addAll(pipeline.shutDown()); + } } if (!errors.isEmpty()) { logIngestModuleErrors(errors); } - if (this.runInteractively) { + if (this.doUI) { // Finish the first stage data source ingest progress bar, if it hasn't // already been finished. synchronized (this.dataSourceIngestProgressLock) { @@ -608,7 +612,7 @@ final class DataSourceIngestJob { logger.log(Level.INFO, "Finished analysis for {0} (jobId={1})", new Object[]{dataSource.getName(), this.id}); this.stage = DataSourceIngestJob.Stages.FINALIZATION; - if (this.runInteractively) { + if (this.doUI) { // Finish the second stage data source ingest progress bar, if it hasn't // already been finished. synchronized (this.dataSourceIngestProgressLock) { @@ -640,7 +644,7 @@ final class DataSourceIngestJob { } } - if (this.runInteractively) { + if (this.doUI) { /** * Shut down the data source ingest progress bar right away. * Data source-level processing is finished for this stage. @@ -679,7 +683,7 @@ final class DataSourceIngestJob { synchronized (this.fileIngestProgressLock) { ++this.processedFiles; - if (this.runInteractively) { + if (this.doUI) { /** * Update the file ingest progress bar. */ @@ -701,7 +705,7 @@ final class DataSourceIngestJob { logIngestModuleErrors(errors); } - if (this.runInteractively && !this.cancelled) { + if (this.doUI && !this.cancelled) { synchronized (this.fileIngestProgressLock) { /** * Update the file ingest progress bar again, in @@ -756,7 +760,7 @@ final class DataSourceIngestJob { * @param displayName The new display name. */ void updateDataSourceIngestProgressBarDisplayName(String displayName) { - if (this.runInteractively && !this.cancelled) { + if (this.doUI && !this.cancelled) { synchronized (this.dataSourceIngestProgressLock) { this.dataSourceIngestProgress.setDisplayName(displayName); } @@ -772,7 +776,7 @@ final class DataSourceIngestJob { * data source. */ void switchDataSourceIngestProgressBarToDeterminate(int workUnits) { - if (this.runInteractively && !this.cancelled) { + if (this.doUI && !this.cancelled) { synchronized (this.dataSourceIngestProgressLock) { if (null != this.dataSourceIngestProgress) { this.dataSourceIngestProgress.switchToDeterminate(workUnits); @@ -787,7 +791,7 @@ final class DataSourceIngestJob { * process the data source is unknown. */ void switchDataSourceIngestProgressBarToIndeterminate() { - if (this.runInteractively && !this.cancelled) { + if (this.doUI && !this.cancelled) { synchronized (this.dataSourceIngestProgressLock) { if (null != this.dataSourceIngestProgress) { this.dataSourceIngestProgress.switchToIndeterminate(); @@ -803,7 +807,7 @@ final class DataSourceIngestJob { * @param workUnits Number of work units performed. */ void advanceDataSourceIngestProgressBar(int workUnits) { - if (this.runInteractively && !this.cancelled) { + if (this.doUI && !this.cancelled) { synchronized (this.dataSourceIngestProgressLock) { if (null != this.dataSourceIngestProgress) { this.dataSourceIngestProgress.progress("", workUnits); @@ -819,7 +823,7 @@ final class DataSourceIngestJob { * @param currentTask The task name. */ void advanceDataSourceIngestProgressBar(String currentTask) { - if (this.runInteractively && !this.cancelled) { + if (this.doUI && !this.cancelled) { synchronized (this.dataSourceIngestProgressLock) { if (null != this.dataSourceIngestProgress) { this.dataSourceIngestProgress.progress(currentTask); @@ -837,7 +841,7 @@ final class DataSourceIngestJob { * @param workUnits Number of work units performed. */ void advanceDataSourceIngestProgressBar(String currentTask, int workUnits) { - if (this.runInteractively && !this.cancelled) { + if (this.doUI && !this.cancelled) { synchronized (this.fileIngestProgressLock) { this.dataSourceIngestProgress.progress(currentTask, workUnits); } @@ -865,7 +869,7 @@ final class DataSourceIngestJob { this.currentDataSourceIngestModuleCancelled = false; this.cancelledDataSourceIngestModules.add(moduleDisplayName); - if (this.runInteractively) { + if (this.doUI) { /** * A new progress bar must be created because the cancel button of * the previously constructed component is disabled by NetBeans when @@ -907,7 +911,7 @@ final class DataSourceIngestJob { * level and file level ingest pipelines. */ void cancel() { - if (this.runInteractively) { + if (this.doUI) { /** * Put a cancellation message on data source level ingest progress * bar, if it is still running. diff --git a/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestModuleProcessTerminator.java b/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestModuleProcessTerminator.java index 8e7dab9851..59b5af0730 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestModuleProcessTerminator.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestModuleProcessTerminator.java @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.ingest; +import org.sleuthkit.autopsy.coreutils.ExecUtil; import org.sleuthkit.autopsy.coreutils.ExecUtil.ProcessTerminator; import org.sleuthkit.autopsy.coreutils.ExecUtil.TimedProcessTerminator; @@ -30,6 +31,7 @@ public final class DataSourceIngestModuleProcessTerminator implements ProcessTer private final IngestJobContext context; private TimedProcessTerminator timedTerminator; + private ProcTerminationCode terminationCode; /** * Constructs a process terminator for a data source ingest module. @@ -38,6 +40,7 @@ public final class DataSourceIngestModuleProcessTerminator implements ProcessTer */ public DataSourceIngestModuleProcessTerminator(IngestJobContext context) { this.context = context; + this.terminationCode = ProcTerminationCode.NONE; } /** @@ -51,6 +54,21 @@ public final class DataSourceIngestModuleProcessTerminator implements ProcessTer this.timedTerminator = new TimedProcessTerminator(maxRunTimeInSeconds); } + /** + * Constructs a process terminator for a data source ingest module. Adds + * ability to use global process termination time out. + * + * @param context The ingest job context for the ingest module. + * @param useGlobalTimeOut Flag whether to use global process termination + * timeout. + */ + public DataSourceIngestModuleProcessTerminator(IngestJobContext context, boolean useGlobalTimeOut) { + this(context); + if (useGlobalTimeOut) { + this.timedTerminator = new ExecUtil.TimedProcessTerminator(); + } + } + /** * @inheritDoc */ @@ -58,10 +76,24 @@ public final class DataSourceIngestModuleProcessTerminator implements ProcessTer public boolean shouldTerminateProcess() { if (this.context.dataSourceIngestIsCancelled()) { + this.terminationCode = ProcTerminationCode.CANCELATION; return true; } - return this.timedTerminator != null ? this.timedTerminator.shouldTerminateProcess() : false; + if (this.timedTerminator != null && this.timedTerminator.shouldTerminateProcess()) { + this.terminationCode = ProcTerminationCode.TIME_OUT; + return true; + } + + return false; } + /** + * Returns process termination code. + * + * @return ProcTerminationCode Process termination code. + */ + public ProcTerminationCode getTerminationCode() { + return this.terminationCode; + } } diff --git a/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestPipeline.java b/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestPipeline.java index e65ef09b7d..c10d1436d2 100755 --- a/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestPipeline.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestPipeline.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014 Basis Technology Corp. + * Copyright 2014-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -98,32 +98,34 @@ final class DataSourceIngestPipeline { */ synchronized List process(DataSourceIngestTask task) { List errors = new ArrayList<>(); - Content dataSource = task.getDataSource(); - for (PipelineModule module : modules) { - try { - this.currentModule = module; - String displayName = NbBundle.getMessage(this.getClass(), - "IngestJob.progress.dataSourceIngest.displayName", - module.getDisplayName(), dataSource.getName()); - this.job.updateDataSourceIngestProgressBarDisplayName(displayName); - this.job.switchDataSourceIngestProgressBarToIndeterminate(); - DataSourceIngestPipeline.ingestManager.setIngestTaskProgress(task, module.getDisplayName()); - logger.log(Level.INFO, "{0} analysis of {1} (jobId={2}) starting", new Object[]{module.getDisplayName(), this.job.getDataSource().getName(), this.job.getDataSource().getId()}); - module.process(dataSource, new DataSourceIngestModuleProgress(this.job)); - logger.log(Level.INFO, "{0} analysis of {1} (jobId={2}) finished", new Object[]{module.getDisplayName(), this.job.getDataSource().getName(), this.job.getDataSource().getId()}); - } catch (Throwable ex) { // Catch-all exception firewall - errors.add(new IngestModuleError(module.getDisplayName(), ex)); - String msg = ex.getMessage(); - // Jython run-time errors don't seem to have a message, but have details in toString. - if (msg == null) { - msg = ex.toString(); + if (!this.job.isCancelled()) { + Content dataSource = task.getDataSource(); + for (PipelineModule module : modules) { + try { + this.currentModule = module; + String displayName = NbBundle.getMessage(this.getClass(), + "IngestJob.progress.dataSourceIngest.displayName", + module.getDisplayName(), dataSource.getName()); + this.job.updateDataSourceIngestProgressBarDisplayName(displayName); + this.job.switchDataSourceIngestProgressBarToIndeterminate(); + DataSourceIngestPipeline.ingestManager.setIngestTaskProgress(task, module.getDisplayName()); + logger.log(Level.INFO, "{0} analysis of {1} (jobId={2}) starting", new Object[]{module.getDisplayName(), this.job.getDataSource().getName(), this.job.getDataSource().getId()}); + module.process(dataSource, new DataSourceIngestModuleProgress(this.job)); + logger.log(Level.INFO, "{0} analysis of {1} (jobId={2}) finished", new Object[]{module.getDisplayName(), this.job.getDataSource().getName(), this.job.getDataSource().getId()}); + } catch (Throwable ex) { // Catch-all exception firewall + errors.add(new IngestModuleError(module.getDisplayName(), ex)); + String msg = ex.getMessage(); + // Jython run-time errors don't seem to have a message, but have details in toString. + if (msg == null) { + msg = ex.toString(); + } + MessageNotifyUtil.Notify.error(module.getDisplayName() + " Error", msg); + } + if (this.job.isCancelled()) { + break; + } else if (this.job.currentDataSourceIngestModuleIsCancelled()) { + this.job.currentDataSourceIngestModuleCancellationCompleted(currentModule.getDisplayName()); } - MessageNotifyUtil.Notify.error(module.getDisplayName() + " Error", msg); - } - if (this.job.isCancelled()) { - break; - } else if (this.job.currentDataSourceIngestModuleIsCancelled()) { - this.job.currentDataSourceIngestModuleCancellationCompleted(currentModule.getDisplayName()); } } this.currentModule = null; diff --git a/Core/src/org/sleuthkit/autopsy/ingest/FileIngestModuleProcessTerminator.java b/Core/src/org/sleuthkit/autopsy/ingest/FileIngestModuleProcessTerminator.java index a8b75ad1be..7f0ac84240 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/FileIngestModuleProcessTerminator.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/FileIngestModuleProcessTerminator.java @@ -30,6 +30,7 @@ public final class FileIngestModuleProcessTerminator implements ProcessTerminato private final IngestJobContext context; private ExecUtil.TimedProcessTerminator timedTerminator; + private ProcTerminationCode terminationCode; /** * Constructs a process terminator for a file ingest module. @@ -38,6 +39,7 @@ public final class FileIngestModuleProcessTerminator implements ProcessTerminato */ public FileIngestModuleProcessTerminator(IngestJobContext context) { this.context = context; + this.terminationCode = ProcTerminationCode.NONE; } /** @@ -51,6 +53,21 @@ public final class FileIngestModuleProcessTerminator implements ProcessTerminato this.timedTerminator = new ExecUtil.TimedProcessTerminator(maxRunTimeInSeconds); } + /** + * Constructs a process terminator for a file ingest module. Adds ability to + * use global process termination time out. + * + * @param context The ingest job context for the ingest module. + * @param useGlobalTimeOut Flag whether to use global process termination + * timeout. + */ + public FileIngestModuleProcessTerminator(IngestJobContext context, boolean useGlobalTimeOut) { + this(context); + if (useGlobalTimeOut) { + this.timedTerminator = new ExecUtil.TimedProcessTerminator(); + } + } + /** * @inheritDoc */ @@ -58,10 +75,24 @@ public final class FileIngestModuleProcessTerminator implements ProcessTerminato public boolean shouldTerminateProcess() { if (this.context.fileIngestIsCancelled()) { + this.terminationCode = ProcTerminationCode.CANCELATION; return true; } - return this.timedTerminator != null ? this.timedTerminator.shouldTerminateProcess() : false; + if (this.timedTerminator != null && this.timedTerminator.shouldTerminateProcess()) { + this.terminationCode = ProcTerminationCode.TIME_OUT; + return true; + } + + return false; } + /** + * Returns process termination code. + * + * @return ProcTerminationCode Process termination code. + */ + public ProcTerminationCode getTerminationCode() { + return this.terminationCode; + } } diff --git a/Core/src/org/sleuthkit/autopsy/ingest/FileIngestPipeline.java b/Core/src/org/sleuthkit/autopsy/ingest/FileIngestPipeline.java index 3f86e6112c..61fe7ae77f 100755 --- a/Core/src/org/sleuthkit/autopsy/ingest/FileIngestPipeline.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/FileIngestPipeline.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014 Basis Technology Corp. + * Copyright 2014-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -114,27 +114,29 @@ final class FileIngestPipeline { */ synchronized List process(FileIngestTask task) { List errors = new ArrayList<>(); - AbstractFile file = task.getFile(); - for (PipelineModule module : this.modules) { - try { - FileIngestPipeline.ingestManager.setIngestTaskProgress(task, module.getDisplayName()); - module.process(file); - } catch (Throwable ex) { // Catch-all exception firewall - errors.add(new IngestModuleError(module.getDisplayName(), ex)); - String msg = ex.getMessage(); - // Jython run-time errors don't seem to have a message, but have details in toString. - if (msg == null) { - msg = ex.toString(); - } - MessageNotifyUtil.Notify.error(module.getDisplayName() + " Error", msg); - } - if (this.job.isCancelled()) { - break; - } - } - file.close(); if (!this.job.isCancelled()) { - IngestManager.getInstance().fireFileIngestDone(file); + AbstractFile file = task.getFile(); + for (PipelineModule module : this.modules) { + try { + FileIngestPipeline.ingestManager.setIngestTaskProgress(task, module.getDisplayName()); + module.process(file); + } catch (Throwable ex) { // Catch-all exception firewall + errors.add(new IngestModuleError(module.getDisplayName(), ex)); + String msg = ex.getMessage(); + // Jython run-time errors don't seem to have a message, but have details in toString. + if (msg == null) { + msg = ex.toString(); + } + MessageNotifyUtil.Notify.error(module.getDisplayName() + " Error", msg); + } + if (this.job.isCancelled()) { + break; + } + } + file.close(); + if (!this.job.isCancelled()) { + IngestManager.getInstance().fireFileIngestDone(file); + } } FileIngestPipeline.ingestManager.setIngestTaskProgressCompleted(task); return errors; @@ -147,17 +149,19 @@ final class FileIngestPipeline { */ synchronized List shutDown() { List errors = new ArrayList<>(); - for (PipelineModule module : this.modules) { - try { - module.shutDown(); - } catch (Throwable ex) { // Catch-all exception firewall - errors.add(new IngestModuleError(module.getDisplayName(), ex)); - String msg = ex.getMessage(); - // Jython run-time errors don't seem to have a message, but have details in toString. - if (msg == null) { - msg = ex.toString(); + if (this.running == true) { // Don't shut down pipelines that never started + for (PipelineModule module : this.modules) { + try { + module.shutDown(); + } catch (Throwable ex) { // Catch-all exception firewall + errors.add(new IngestModuleError(module.getDisplayName(), ex)); + String msg = ex.getMessage(); + // Jython run-time errors don't seem to have a message, but have details in toString. + if (msg == null) { + msg = ex.toString(); + } + MessageNotifyUtil.Notify.error(module.getDisplayName() + " Error", msg); } - MessageNotifyUtil.Notify.error(module.getDisplayName() + " Error", msg); } } this.running = false; diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestJob.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestJob.java index 967993eca3..941e8de8ca 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestJob.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestJob.java @@ -48,16 +48,16 @@ public final class IngestJob { * Constructs an ingest job that runs a collection of data sources through a * set of ingest modules specified via ingest job settings. * - * @param dataSources The data sources to be ingested. - * @param settings The ingest job settings. - * @param runInteractively Whether or not this job should use progress bars, - * message boxes for errors, etc. + * @param dataSources The data sources to be ingested. + * @param settings The ingest job settings. + * @param doUI Whether or not this job should use progress bars, + * message boxes for errors, etc. */ - IngestJob(Collection dataSources, IngestJobSettings settings, boolean runInteractively) { + IngestJob(Collection dataSources, IngestJobSettings settings, boolean doUI) { this.id = IngestJob.nextId.getAndIncrement(); this.dataSourceJobs = new ConcurrentHashMap<>(); for (Content dataSource : dataSources) { - DataSourceIngestJob dataSourceIngestJob = new DataSourceIngestJob(this, dataSource, settings, runInteractively); + DataSourceIngestJob dataSourceIngestJob = new DataSourceIngestJob(this, dataSource, settings, doUI); this.dataSourceJobs.put(dataSourceIngestJob.getId(), dataSourceIngestJob); } incompleteJobsCount = new AtomicInteger(dataSourceJobs.size()); @@ -73,12 +73,19 @@ public final class IngestJob { } /** - * Checks to see if this ingest job has at least one ingest pipeline when - * its settings are applied. + * Checks to see if this ingest job has at least one non-empty ingest module + * pipeline (first or second stage data-source-level pipeline or file-level + * pipeline). * * @return True or false. */ boolean hasIngestPipeline() { + /** + * TODO: This could actually be done more simply by adding a method to + * the IngestJobSettings to check for at least one enabled ingest module + * template. The test could then be done in the ingest manager before + * even constructing an ingest job. + */ for (DataSourceIngestJob dataSourceJob : this.dataSourceJobs.values()) { if (dataSourceJob.hasIngestPipeline()) { return true; @@ -101,27 +108,19 @@ public final class IngestJob { } started = true; + List startedDataSourceJobs = new ArrayList<>(); for (DataSourceIngestJob dataSourceJob : this.dataSourceJobs.values()) { errors.addAll(dataSourceJob.start()); - if (!errors.isEmpty()) { + if (errors.isEmpty()) { + IngestManager.getInstance().fireDataSourceAnalysisStarted(id, dataSourceJob.getId(), dataSourceJob.getDataSource()); + startedDataSourceJobs.add(dataSourceJob); + } else { + startedDataSourceJobs.stream().forEach((startedDataSourceJob) -> { + startedDataSourceJob.cancel(); + }); break; } } - - /** - * TODO: Need to let successfully started data source ingest jobs know - * they should shut down. This means that the start up of the ingest - * module pipelines and the submission of ingest tasks should be - * separated. This cancellation is just a stop gap; fortunately, if - * startup is going to fail, it will likely fail for the first child - * data source ingest job. - */ - if (!errors.isEmpty()) { - for (DataSourceIngestJob dataSourceJob : this.dataSourceJobs.values()) { - dataSourceJob.cancel(); - } - } - return errors; } @@ -151,9 +150,9 @@ public final class IngestJob { */ List getDataSourceIngestJobSnapshots() { List snapshots = new ArrayList<>(); - for (DataSourceIngestJob dataSourceJob : this.dataSourceJobs.values()) { + this.dataSourceJobs.values().stream().forEach((dataSourceJob) -> { snapshots.add(dataSourceJob.getSnapshot(true)); - } + }); return snapshots; } @@ -163,10 +162,11 @@ public final class IngestJob { * but there may be a delay before all of the ingest modules in the * pipelines respond by stopping processing. */ - public void cancel() { - for (DataSourceIngestJob job : this.dataSourceJobs.values()) { + synchronized public void cancel() { + IngestManager ingestManager = IngestManager.getInstance(); + this.dataSourceJobs.values().stream().forEach((job) -> { job.cancel(); - } + }); this.cancelled = true; } @@ -184,11 +184,17 @@ public final class IngestJob { * Provides a callback for completed data source ingest jobs, allowing this * ingest job to notify the ingest manager when it is complete. * - * @param dataSourceIngestJob A completed data source ingest job. + * @param job A completed data source ingest job. */ - void dataSourceJobFinished(DataSourceIngestJob dataSourceIngestJob) { + void dataSourceJobFinished(DataSourceIngestJob job) { + IngestManager ingestManager = IngestManager.getInstance(); + if (!job.isCancelled()) { + ingestManager.fireDataSourceAnalysisCompleted(id, job.getId(), job.getDataSource()); + } else { + IngestManager.getInstance().fireDataSourceAnalysisCancelled(id, job.getId(), job.getDataSource()); + } if (incompleteJobsCount.decrementAndGet() == 0) { - IngestManager.getInstance().finishIngestJob(this); + ingestManager.finishIngestJob(this); } } diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestJobSettings.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestJobSettings.java index d0cb616e06..d31c0da8c7 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestJobSettings.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestJobSettings.java @@ -145,6 +145,15 @@ public class IngestJobSettings { void setProcessUnallocatedSpace(boolean processUnallocatedSpace) { this.processUnallocatedSpace = processUnallocatedSpace; } + + /** + * Returns the path to the ingest module settings folder. + * + * @return path to the module settings folder + */ + public Path getSavedModuleSettingsFolder(){ + return Paths.get(IngestJobSettings.MODULE_SETTINGS_FOLDER_PATH, context); + } /** * Creates the folder for saving the individual ingest module settings part @@ -152,7 +161,7 @@ public class IngestJobSettings { */ private void createSavedModuleSettingsFolder() { try { - Path folder = Paths.get(IngestJobSettings.MODULE_SETTINGS_FOLDER_PATH, context); + Path folder = getSavedModuleSettingsFolder(); Files.createDirectories(folder); this.moduleSettingsFolderPath = folder.toAbsolutePath().toString(); } catch (IOException | SecurityException ex) { diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestJobSettingsPanel.form b/Core/src/org/sleuthkit/autopsy/ingest/IngestJobSettingsPanel.form index 08b078aba9..2f769e6f8b 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestJobSettingsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestJobSettingsPanel.form @@ -34,28 +34,48 @@ - - + + + + + + + + + + + + + + + + - - - + + - + - + + + + - + + + + + + - @@ -109,13 +129,13 @@ - + - + - + @@ -186,52 +206,38 @@ - + - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestJobSettingsPanel.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestJobSettingsPanel.java index b39fd9d7b5..78c1a214d8 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestJobSettingsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestJobSettingsPanel.java @@ -142,7 +142,8 @@ public final class IngestJobSettingsPanel extends javax.swing.JPanel { descriptionLabel = new javax.swing.JLabel(); jScrollPane1 = new javax.swing.JScrollPane(); simplePanel = new javax.swing.JPanel(); - processUnallocPanel = new javax.swing.JPanel(); + jButtonSelectAll = new javax.swing.JButton(); + jButtonDeselectAll = new javax.swing.JButton(); processUnallocCheckbox = new javax.swing.JCheckBox(); setMaximumSize(new java.awt.Dimension(5750, 3000)); @@ -190,13 +191,13 @@ public final class IngestJobSettingsPanel extends javax.swing.JPanel { jPanel1Layout.setHorizontalGroup( jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(jSeparator2, javax.swing.GroupLayout.Alignment.TRAILING) - .addGroup(jPanel1Layout.createSequentialGroup() + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel1Layout.createSequentialGroup() .addContainerGap() - .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addGroup(jPanel1Layout.createSequentialGroup() .addComponent(descriptionLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 203, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 22, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(advancedButton))) .addContainerGap()) ); @@ -214,7 +215,19 @@ public final class IngestJobSettingsPanel extends javax.swing.JPanel { .addContainerGap()) ); - processUnallocPanel.setBorder(javax.swing.BorderFactory.createLineBorder(new java.awt.Color(160, 160, 160))); + jButtonSelectAll.setText(org.openide.util.NbBundle.getMessage(IngestJobSettingsPanel.class, "IngestJobSettingsPanel.jButtonSelectAll.text")); // NOI18N + jButtonSelectAll.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + jButtonSelectAllActionPerformed(evt); + } + }); + + jButtonDeselectAll.setText(org.openide.util.NbBundle.getMessage(IngestJobSettingsPanel.class, "IngestJobSettingsPanel.jButtonDeselectAll.text")); // NOI18N + jButtonDeselectAll.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + jButtonDeselectAllActionPerformed(evt); + } + }); processUnallocCheckbox.setText(org.openide.util.NbBundle.getMessage(IngestJobSettingsPanel.class, "IngestJobSettingsPanel.processUnallocCheckbox.text")); // NOI18N processUnallocCheckbox.setToolTipText(org.openide.util.NbBundle.getMessage(IngestJobSettingsPanel.class, "IngestJobSettingsPanel.processUnallocCheckbox.toolTipText")); // NOI18N @@ -224,23 +237,6 @@ public final class IngestJobSettingsPanel extends javax.swing.JPanel { } }); - javax.swing.GroupLayout processUnallocPanelLayout = new javax.swing.GroupLayout(processUnallocPanel); - processUnallocPanel.setLayout(processUnallocPanelLayout); - processUnallocPanelLayout.setHorizontalGroup( - processUnallocPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(processUnallocPanelLayout.createSequentialGroup() - .addContainerGap() - .addComponent(processUnallocCheckbox) - .addContainerGap(60, Short.MAX_VALUE)) - ); - processUnallocPanelLayout.setVerticalGroup( - processUnallocPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(processUnallocPanelLayout.createSequentialGroup() - .addContainerGap() - .addComponent(processUnallocCheckbox) - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - ); - javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( @@ -248,10 +244,19 @@ public final class IngestJobSettingsPanel extends javax.swing.JPanel { .addGroup(layout.createSequentialGroup() .addContainerGap() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(processUnallocPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(modulesScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(jPanel1, javax.swing.GroupLayout.PREFERRED_SIZE, 328, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addComponent(modulesScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGap(10, 10, 10)) + .addGroup(layout.createSequentialGroup() + .addGap(24, 24, 24) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(processUnallocCheckbox) + .addGroup(layout.createSequentialGroup() + .addComponent(jButtonSelectAll, javax.swing.GroupLayout.PREFERRED_SIZE, 86, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(18, 18, 18) + .addComponent(jButtonDeselectAll))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 30, Short.MAX_VALUE))) + .addComponent(jPanel1, javax.swing.GroupLayout.DEFAULT_SIZE, 310, Short.MAX_VALUE) .addContainerGap()) ); layout.setVerticalGroup( @@ -259,19 +264,20 @@ public final class IngestJobSettingsPanel extends javax.swing.JPanel { .addGroup(layout.createSequentialGroup() .addContainerGap() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(jPanel1, javax.swing.GroupLayout.DEFAULT_SIZE, 278, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addComponent(jPanel1, javax.swing.GroupLayout.DEFAULT_SIZE, 278, Short.MAX_VALUE) + .addContainerGap()) .addGroup(layout.createSequentialGroup() .addComponent(modulesScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(processUnallocPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) - .addContainerGap()) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(jButtonDeselectAll) + .addComponent(jButtonSelectAll)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(processUnallocCheckbox, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE)))) ); }// //GEN-END:initComponents - private void processUnallocCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_processUnallocCheckboxActionPerformed - this.settings.setProcessUnallocatedSpace(processUnallocCheckbox.isSelected()); - }//GEN-LAST:event_processUnallocCheckboxActionPerformed - private void advancedButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_advancedButtonActionPerformed final AdvancedConfigurationDialog dialog = new AdvancedConfigurationDialog(); @@ -295,16 +301,36 @@ public final class IngestJobSettingsPanel extends javax.swing.JPanel { dialog.display(selectedModule.getGlobalSettingsPanel()); }//GEN-LAST:event_advancedButtonActionPerformed + private void jButtonSelectAllActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButtonSelectAllActionPerformed + SelectAllModules(true); + }//GEN-LAST:event_jButtonSelectAllActionPerformed + + private void jButtonDeselectAllActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButtonDeselectAllActionPerformed + SelectAllModules(false); + }//GEN-LAST:event_jButtonDeselectAllActionPerformed + + private void processUnallocCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_processUnallocCheckboxActionPerformed + this.settings.setProcessUnallocatedSpace(processUnallocCheckbox.isSelected()); + }//GEN-LAST:event_processUnallocCheckboxActionPerformed + + private void SelectAllModules(boolean set) { + for (IngestModuleModel module : modules) { + module.setEnabled(set); + } + modulesTable.repaint(); + } + // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JButton advancedButton; private javax.swing.JLabel descriptionLabel; + private javax.swing.JButton jButtonDeselectAll; + private javax.swing.JButton jButtonSelectAll; private javax.swing.JPanel jPanel1; private javax.swing.JScrollPane jScrollPane1; private javax.swing.JSeparator jSeparator2; private javax.swing.JScrollPane modulesScrollPane; private javax.swing.JTable modulesTable; private javax.swing.JCheckBox processUnallocCheckbox; - private javax.swing.JPanel processUnallocPanel; private javax.swing.JPanel simplePanel; private javax.swing.ButtonGroup timeGroup; // End of variables declaration//GEN-END:variables diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestManager.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestManager.java index 2b09a9b436..eb3de76a61 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestManager.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestManager.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-2014 Basis Technology Corp. + * Copyright 2013-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,13 +22,14 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.awt.EventQueue; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; -import java.beans.PropertyChangeSupport; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; @@ -36,15 +37,27 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.swing.JOptionPane; import org.netbeans.api.progress.ProgressHandle; import org.netbeans.api.progress.ProgressHandleFactory; import org.openide.util.Cancellable; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.core.RuntimeProperties; +import org.sleuthkit.autopsy.core.ServicesMonitor; import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.autopsy.events.AutopsyEvent; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.events.AutopsyEventException; +import org.sleuthkit.autopsy.events.AutopsyEventPublisher; +import org.sleuthkit.autopsy.ingest.events.BlackboardPostEvent; +import org.sleuthkit.autopsy.ingest.events.ContentChangedEvent; +import org.sleuthkit.autopsy.ingest.events.DataSourceAnalysisCompletedEvent; +import org.sleuthkit.autopsy.ingest.events.DataSourceAnalysisStartedEvent; +import org.sleuthkit.autopsy.ingest.events.FileAnalyzedEvent; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.Content; @@ -56,6 +69,7 @@ public class IngestManager { private static final Logger logger = Logger.getLogger(IngestManager.class.getName()); private static IngestManager instance; + private final Object ingestMessageBoxLock = new Object(); /** * The ingest manager maintains a mapping of ingest job IDs to running @@ -92,15 +106,17 @@ public class IngestManager { private int numberOfFileIngestThreads; private final ExecutorService fileIngestThreadPool; - /** - * The ingest manager uses the property change feature from Java Beans as an - * event publishing mechanism. There are two kinds of events, ingest job - * events and ingest module events. Property changes are fired by ingest - * event publishers on a pool thread. - */ - private final PropertyChangeSupport ingestJobEventPublisher; - private final PropertyChangeSupport ingestModuleEventPublisher; - private final ExecutorService fireIngestEventsThreadPool; + private static final String JOB_EVENT_CHANNEL_NAME = "%s-Ingest-Job-Events"; + private static final String MODULE_EVENT_CHANNEL_NAME = "%s-Ingest-Module-Events"; + private static final Set jobEventNames = Stream.of(IngestJobEvent.values()) + .map(IngestJobEvent::toString) + .collect(Collectors.toSet()); + private static final Set moduleEventNames = Stream.of(IngestModuleEvent.values()) + .map(IngestModuleEvent::toString) + .collect(Collectors.toSet()); + private AutopsyEventPublisher jobEventPublisher; + private AutopsyEventPublisher moduleEventPublisher; + private final ExecutorService eventPublishingExecutor; /** * The ingest manager uses an ingest monitor to determine when system @@ -133,11 +149,10 @@ public class IngestManager { private volatile boolean jobCreationIsEnabled; /** - * The ingest manager can be directed to forgo use of message boxes, the - * ingest message box, NetBeans progress handles, etc. Running interactively - * is the default. + * Ingest manager subscribes to service outage notifications. If key + * services are down, ingest manager cancels all ingest jobs in progress. */ - private volatile boolean runInteractively; + private final ServicesMonitor servicesMonitor; /** * Ingest job events. @@ -162,6 +177,24 @@ public class IngestManager { * and the new value is set to null. */ CANCELLED, + /** + * Property change event fired when analysis (ingest) of a data source + * included in an ingest job is started. Both the old and new values of + * the ProerptyChangeEvent are set to null - cast the + * PropertyChangeEvent to + * org.sleuthkit.autopsy.ingest.events.DataSourceAnalysisStartedEvent to + * access event data. + */ + DATA_SOURCE_ANALYSIS_STARTED, + /** + * Property change event fired when analysis (ingest) of a data source + * included in an ingest job is completed. Both the old and new values + * of the ProerptyChangeEvent are set to null - cast the + * PropertyChangeEvent to + * org.sleuthkit.autopsy.ingest.events.DataSourceAnalysisCompletedEvent + * to access event data. + */ + DATA_SOURCE_ANALYSIS_COMPLETED, }; /** @@ -221,20 +254,22 @@ public class IngestManager { * runs. */ private IngestManager() { - this.runInteractively = true; this.ingestModuleRunTimes = new ConcurrentHashMap<>(); this.ingestThreadActivitySnapshots = new ConcurrentHashMap<>(); this.ingestErrorMessagePosts = new AtomicLong(0L); this.ingestMonitor = new IngestMonitor(); - this.ingestModuleEventPublisher = new PropertyChangeSupport(IngestManager.class); - this.fireIngestEventsThreadPool = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("IM-ingest-events-%d").build()); //NON-NLS - this.ingestJobEventPublisher = new PropertyChangeSupport(IngestManager.class); + this.eventPublishingExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("IM-ingest-events-%d").build()); //NON-NLS + this.jobEventPublisher = new AutopsyEventPublisher(); + this.moduleEventPublisher = new AutopsyEventPublisher(); this.dataSourceIngestThreadPool = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("IM-data-source-ingest-%d").build()); //NON-NLS this.startIngestJobsThreadPool = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("IM-start-ingest-jobs-%d").build()); //NON-NLS this.nextThreadId = new AtomicLong(0L); this.jobsById = new ConcurrentHashMap<>(); this.ingestJobStarters = new ConcurrentHashMap<>(); + this.servicesMonitor = ServicesMonitor.getInstance(); + subscribeToServiceMonitorEvents(); + this.startDataSourceIngestThread(); numberOfFileIngestThreads = UserPreferences.numberOfFileIngestThreads(); @@ -254,7 +289,7 @@ public class IngestManager { */ private void startDataSourceIngestThread() { long threadId = nextThreadId.incrementAndGet(); - dataSourceIngestThreadPool.submit(new IngestTaskExecuter(threadId, IngestTasksScheduler.getInstance().getDataSourceIngestTaskQueue())); + dataSourceIngestThreadPool.submit(new ExecuteIngestJobsTask(threadId, IngestTasksScheduler.getInstance().getDataSourceIngestTaskQueue())); ingestThreadActivitySnapshots.put(threadId, new IngestThreadActivitySnapshot(threadId)); } @@ -264,46 +299,117 @@ public class IngestManager { */ private void startFileIngestThread() { long threadId = nextThreadId.incrementAndGet(); - fileIngestThreadPool.submit(new IngestTaskExecuter(threadId, IngestTasksScheduler.getInstance().getFileIngestTaskQueue())); + fileIngestThreadPool.submit(new ExecuteIngestJobsTask(threadId, IngestTasksScheduler.getInstance().getFileIngestTaskQueue())); ingestThreadActivitySnapshots.put(threadId, new IngestThreadActivitySnapshot(threadId)); } + /** + * Subscribes this ingest manager to local and remote case-related events. + */ private void subscribeToCaseEvents() { - Case.addPropertyChangeListener(new PropertyChangeListener() { + Case.addEventSubscriber(Case.Events.CURRENT_CASE.toString(), new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent event) { - if (event.getPropertyName().equals(Case.Events.CURRENT_CASE.toString())) { - if (event.getNewValue() != null) { - handleCaseOpened(); - } else { - handleCaseClosed(); - } + if (event.getNewValue() != null) { + handleCaseOpened(); + } else { + handleCaseClosed(); } } }); } + /** + * Subscribe ingest manager to service monitor events. Cancels ingest if one + * of services it's subscribed to goes down. + */ + private void subscribeToServiceMonitorEvents() { + PropertyChangeListener propChangeListener = new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (evt.getNewValue().equals(ServicesMonitor.ServiceStatus.DOWN.toString())) { + + // check whether a milti-user case is currently being processed + try { + if (!Case.isCaseOpen() || Case.getCurrentCase().getCaseType() != Case.CaseType.MULTI_USER_CASE) { + return; + } + } catch (IllegalStateException ignore) { + // thorown by Case.getCurrentCase() when no case is open + return; + } + + // one of the services we subscribed to went down + String serviceDisplayName = ServicesMonitor.Service.valueOf(evt.getPropertyName()).getDisplayName(); + logger.log(Level.SEVERE, "Service {0} is down! Cancelling all running ingest jobs", serviceDisplayName); //NON-NLS + + // display notification if running interactively + if (isIngestRunning() && RuntimeProperties.coreComponentsAreActive()) { + EventQueue.invokeLater(new Runnable() { + @Override + public void run() { + JOptionPane.showMessageDialog(null, + NbBundle.getMessage(this.getClass(), "IngestManager.cancellingIngest.msgDlg.text"), + NbBundle.getMessage(this.getClass(), "IngestManager.serviceIsDown.msgDlg.text", serviceDisplayName), + JOptionPane.ERROR_MESSAGE); + } + }); + } + + // cancel ingest if running + cancelAllIngestJobs(); + } + } + }; + + // subscribe to services of interest + Set servicesList = new HashSet<>(); + servicesList.add(ServicesMonitor.Service.REMOTE_CASE_DATABASE.toString()); + servicesList.add(ServicesMonitor.Service.REMOTE_KEYWORD_SEARCH.toString()); + this.servicesMonitor.addSubscriber(servicesList, propChangeListener); + } + synchronized void handleCaseOpened() { this.jobCreationIsEnabled = true; clearIngestMessageBox(); + try { + /** + * Use the text index name as the remote event channel name prefix + * since it is unique, the same as the case database name for a + * multiuser case, and is readily available through the + * Case.getTextIndexName() API. + */ + Case openedCase = Case.getCurrentCase(); + String channelPrefix = openedCase.getTextIndexName(); + if (Case.CaseType.MULTI_USER_CASE == openedCase.getCaseType()) { + jobEventPublisher.openRemoteEventChannel(String.format(JOB_EVENT_CHANNEL_NAME, channelPrefix)); + moduleEventPublisher.openRemoteEventChannel(String.format(MODULE_EVENT_CHANNEL_NAME, channelPrefix)); + } + } catch (IllegalStateException | AutopsyEventException ex) { + logger.log(Level.SEVERE, "Failed to open remote events channel", ex); //NON-NLS + MessageNotifyUtil.Notify.error(NbBundle.getMessage(IngestManager.class, "IngestManager.OpenEventChannel.Fail.Title"), + NbBundle.getMessage(IngestManager.class, "IngestManager.OpenEventChannel.Fail.ErrMsg")); + } } synchronized void handleCaseClosed() { + jobEventPublisher.closeRemoteEventChannel(); + moduleEventPublisher.closeRemoteEventChannel(); this.jobCreationIsEnabled = false; cancelAllIngestJobs(); clearIngestMessageBox(); } /** - * The ingest manager can be directed to forgo use of message boxes, the - * ingest message box, NetBeans progress handles, etc. Running interactively - * is the default. + * Deprecated, use RuntimeProperties.setCoreComponentsActive instead. * - * @param runInteractively whether or not to this ingest manager should run - * ingest interactively. + * @param runInteractively True or false + * + * @deprecated */ + @Deprecated public synchronized void setRunInteractively(boolean runInteractively) { - this.runInteractively = runInteractively; + RuntimeProperties.setCoreComponentsActive(runInteractively); } /** @@ -312,7 +418,9 @@ public class IngestManager { * display ingest messages. */ void initIngestMessageInbox() { - ingestMessageBox = IngestMessageTopComponent.findInstance(); + synchronized (this.ingestMessageBoxLock) { + ingestMessageBox = IngestMessageTopComponent.findInstance(); + } } /** @@ -320,30 +428,34 @@ public class IngestManager { * * @param message The message to be posted. */ - synchronized void postIngestMessage(IngestMessage message) { - if (ingestMessageBox != null && this.runInteractively) { - if (message.getMessageType() != IngestMessage.MessageType.ERROR && message.getMessageType() != IngestMessage.MessageType.WARNING) { - ingestMessageBox.displayMessage(message); - } else { - long errorPosts = ingestErrorMessagePosts.incrementAndGet(); - if (errorPosts <= MAX_ERROR_MESSAGE_POSTS) { + void postIngestMessage(IngestMessage message) { + synchronized (this.ingestMessageBoxLock) { + if (ingestMessageBox != null && RuntimeProperties.coreComponentsAreActive()) { + if (message.getMessageType() != IngestMessage.MessageType.ERROR && message.getMessageType() != IngestMessage.MessageType.WARNING) { ingestMessageBox.displayMessage(message); - } else if (errorPosts == MAX_ERROR_MESSAGE_POSTS + 1) { - IngestMessage errorMessageLimitReachedMessage = IngestMessage.createErrorMessage( - NbBundle.getMessage(this.getClass(), "IngestManager.IngestMessage.ErrorMessageLimitReached.title"), - NbBundle.getMessage(this.getClass(), "IngestManager.IngestMessage.ErrorMessageLimitReached.subject"), - NbBundle.getMessage(this.getClass(), "IngestManager.IngestMessage.ErrorMessageLimitReached.msg", MAX_ERROR_MESSAGE_POSTS)); - ingestMessageBox.displayMessage(errorMessageLimitReachedMessage); + } else { + long errorPosts = ingestErrorMessagePosts.incrementAndGet(); + if (errorPosts <= MAX_ERROR_MESSAGE_POSTS) { + ingestMessageBox.displayMessage(message); + } else if (errorPosts == MAX_ERROR_MESSAGE_POSTS + 1) { + IngestMessage errorMessageLimitReachedMessage = IngestMessage.createErrorMessage( + NbBundle.getMessage(this.getClass(), "IngestManager.IngestMessage.ErrorMessageLimitReached.title"), + NbBundle.getMessage(this.getClass(), "IngestManager.IngestMessage.ErrorMessageLimitReached.subject"), + NbBundle.getMessage(this.getClass(), "IngestManager.IngestMessage.ErrorMessageLimitReached.msg", MAX_ERROR_MESSAGE_POSTS)); + ingestMessageBox.displayMessage(errorMessageLimitReachedMessage); + } } } } } private void clearIngestMessageBox() { - if (ingestMessageBox != null) { - ingestMessageBox.clearMessages(); + synchronized (this.ingestMessageBoxLock) { + if (ingestMessageBox != null) { + ingestMessageBox.clearMessages(); + } + ingestErrorMessagePosts.set(0); } - ingestErrorMessagePosts.set(0); } /** @@ -365,10 +477,10 @@ public class IngestManager { */ public synchronized void queueIngestJob(Collection dataSources, IngestJobSettings settings) { if (this.jobCreationIsEnabled) { - IngestJob job = new IngestJob(dataSources, settings, this.runInteractively); + IngestJob job = new IngestJob(dataSources, settings, RuntimeProperties.coreComponentsAreActive()); if (job.hasIngestPipeline()) { long taskId = nextThreadId.incrementAndGet(); - Future task = startIngestJobsThreadPool.submit(new IngestJobStarter(taskId, job)); + Future task = startIngestJobsThreadPool.submit(new StartIngestJobTask(taskId, job)); ingestJobStarters.put(taskId, task); } } @@ -384,7 +496,7 @@ public class IngestManager { */ public synchronized IngestJob startIngestJob(Collection dataSources, IngestJobSettings settings) { if (this.jobCreationIsEnabled) { - IngestJob job = new IngestJob(dataSources, settings, this.runInteractively); + IngestJob job = new IngestJob(dataSources, settings, RuntimeProperties.coreComponentsAreActive()); if (job.hasIngestPipeline()) { if (this.startIngestJob(job)) { return job; @@ -407,10 +519,35 @@ public class IngestManager { /** * TODO: This is not really reliable. */ - if (runInteractively && jobsById.size() == 1) { + if (RuntimeProperties.coreComponentsAreActive() && jobsById.size() == 1) { clearIngestMessageBox(); } + // multi-user cases must have multi-user database service running + if (Case.getCurrentCase().getCaseType() == Case.CaseType.MULTI_USER_CASE) { + try { + if (!servicesMonitor.getServiceStatus(ServicesMonitor.Service.REMOTE_CASE_DATABASE.toString()).equals(ServicesMonitor.ServiceStatus.UP.toString())) { + // display notification if running interactively + if (RuntimeProperties.coreComponentsAreActive()) { + EventQueue.invokeLater(new Runnable() { + @Override + public void run() { + String serviceDisplayName = ServicesMonitor.Service.REMOTE_CASE_DATABASE.getDisplayName(); + JOptionPane.showMessageDialog(null, + NbBundle.getMessage(this.getClass(), "IngestManager.cancellingIngest.msgDlg.text"), + NbBundle.getMessage(this.getClass(), "IngestManager.serviceIsDown.msgDlg.text", serviceDisplayName), + JOptionPane.ERROR_MESSAGE); + } + }); + } + // abort ingest + return false; + } + } catch (ServicesMonitor.ServicesMonitorException ignore) { + return false; + } + } + if (!ingestMonitor.isRunning()) { ingestMonitor.start(); } @@ -433,7 +570,7 @@ public class IngestManager { logger.log(Level.SEVERE, String.format("Error starting %s ingest module", error.getModuleDisplayName()), error.getModuleError()); //NON-NLS } IngestManager.logger.log(Level.INFO, "Ingest job {0} could not be started", job.getId()); //NON-NLS - if (this.runInteractively) { + if (RuntimeProperties.coreComponentsAreActive()) { EventQueue.invokeLater(new Runnable() { @Override @@ -510,7 +647,7 @@ public class IngestManager { * @param listener The PropertyChangeListener to register. */ public void addIngestJobEventListener(final PropertyChangeListener listener) { - ingestJobEventPublisher.addPropertyChangeListener(listener); + jobEventPublisher.addSubscriber(jobEventNames, listener); } /** @@ -519,7 +656,7 @@ public class IngestManager { * @param listener The PropertyChangeListener to unregister. */ public void removeIngestJobEventListener(final PropertyChangeListener listener) { - ingestJobEventPublisher.removePropertyChangeListener(listener); + jobEventPublisher.removeSubscriber(jobEventNames, listener); } /** @@ -528,7 +665,7 @@ public class IngestManager { * @param listener The PropertyChangeListener to register. */ public void addIngestModuleEventListener(final PropertyChangeListener listener) { - ingestModuleEventPublisher.addPropertyChangeListener(listener); + moduleEventPublisher.addSubscriber(moduleEventNames, listener); } /** @@ -537,7 +674,7 @@ public class IngestManager { * @param listener The PropertyChangeListener to unregister. */ public void removeIngestModuleEventListener(final PropertyChangeListener listener) { - ingestModuleEventPublisher.removePropertyChangeListener(listener); + moduleEventPublisher.removeSubscriber(moduleEventNames, listener); } /** @@ -550,8 +687,8 @@ public class IngestManager { */ @Deprecated public static void addPropertyChangeListener(final PropertyChangeListener listener) { - instance.ingestJobEventPublisher.addPropertyChangeListener(listener); - instance.ingestModuleEventPublisher.addPropertyChangeListener(listener); + instance.jobEventPublisher.addSubscriber(jobEventNames, listener); + instance.moduleEventPublisher.addSubscriber(moduleEventNames, listener); } /** @@ -564,8 +701,8 @@ public class IngestManager { */ @Deprecated public static void removePropertyChangeListener(final PropertyChangeListener listener) { - instance.ingestJobEventPublisher.removePropertyChangeListener(listener); - instance.ingestModuleEventPublisher.removePropertyChangeListener(listener); + instance.jobEventPublisher.removeSubscriber(jobEventNames, listener); + instance.moduleEventPublisher.removeSubscriber(moduleEventNames, listener); } /** @@ -574,7 +711,8 @@ public class IngestManager { * @param ingestJobId The ingest job id. */ void fireIngestJobStarted(long ingestJobId) { - fireIngestEventsThreadPool.submit(new IngestEventPublisher(ingestJobEventPublisher, IngestJobEvent.STARTED, ingestJobId, null)); + AutopsyEvent event = new AutopsyEvent(IngestJobEvent.STARTED.toString(), ingestJobId, null); + eventPublishingExecutor.submit(new PublishEventTask(event, jobEventPublisher)); } /** @@ -583,7 +721,8 @@ public class IngestManager { * @param ingestJobId The ingest job id. */ void fireIngestJobCompleted(long ingestJobId) { - fireIngestEventsThreadPool.submit(new IngestEventPublisher(ingestJobEventPublisher, IngestJobEvent.COMPLETED, ingestJobId, null)); + AutopsyEvent event = new AutopsyEvent(IngestJobEvent.COMPLETED.toString(), ingestJobId, null); + eventPublishingExecutor.submit(new PublishEventTask(event, jobEventPublisher)); } /** @@ -592,7 +731,44 @@ public class IngestManager { * @param ingestJobId The ingest job id. */ void fireIngestJobCancelled(long ingestJobId) { - fireIngestEventsThreadPool.submit(new IngestEventPublisher(ingestJobEventPublisher, IngestJobEvent.CANCELLED, ingestJobId, null)); + AutopsyEvent event = new AutopsyEvent(IngestJobEvent.CANCELLED.toString(), ingestJobId, null); + eventPublishingExecutor.submit(new PublishEventTask(event, jobEventPublisher)); + } + + /** + * Fire an ingest event signifying analysis of a data source started. + * + * @param ingestJobId The ingest job id. + * @param dataSourceIngestJobId The data source ingest job id. + * @param dataSource The data source. + */ + void fireDataSourceAnalysisStarted(long ingestJobId, long dataSourceIngestJobId, Content dataSource) { + AutopsyEvent event = new DataSourceAnalysisStartedEvent(ingestJobId, dataSourceIngestJobId, dataSource); + eventPublishingExecutor.submit(new PublishEventTask(event, jobEventPublisher)); + } + + /** + * Fire an ingest event signifying analysis of a data source finished. + * + * @param ingestJobId The ingest job id. + * @param dataSourceIngestJobId The data source ingest job id. + * @param dataSource The data source. + */ + void fireDataSourceAnalysisCompleted(long ingestJobId, long dataSourceIngestJobId, Content dataSource) { + AutopsyEvent event = new DataSourceAnalysisCompletedEvent(ingestJobId, dataSourceIngestJobId, dataSource, DataSourceAnalysisCompletedEvent.Reason.ANALYSIS_COMPLETED); + eventPublishingExecutor.submit(new PublishEventTask(event, jobEventPublisher)); + } + + /** + * Fire an ingest event signifying analysis of a data source was canceled. + * + * @param ingestJobId The ingest job id. + * @param dataSourceIngestJobId The data source ingest job id. + * @param dataSource The data source. + */ + void fireDataSourceAnalysisCancelled(long ingestJobId, long dataSourceIngestJobId, Content dataSource) { + AutopsyEvent event = new DataSourceAnalysisCompletedEvent(ingestJobId, dataSourceIngestJobId, dataSource, DataSourceAnalysisCompletedEvent.Reason.ANALYSIS_CANCELLED); + eventPublishingExecutor.submit(new PublishEventTask(event, jobEventPublisher)); } /** @@ -601,7 +777,8 @@ public class IngestManager { * @param file The file that is completed. */ void fireFileIngestDone(AbstractFile file) { - fireIngestEventsThreadPool.submit(new IngestEventPublisher(ingestModuleEventPublisher, IngestModuleEvent.FILE_DONE, file.getId(), file)); + AutopsyEvent event = new FileAnalyzedEvent(file); + eventPublishingExecutor.submit(new PublishEventTask(event, moduleEventPublisher)); } /** @@ -610,7 +787,8 @@ public class IngestManager { * @param moduleDataEvent A ModuleDataEvent with the details of the posting. */ void fireIngestModuleDataEvent(ModuleDataEvent moduleDataEvent) { - fireIngestEventsThreadPool.submit(new IngestEventPublisher(ingestModuleEventPublisher, IngestModuleEvent.DATA_ADDED, moduleDataEvent, null)); + AutopsyEvent event = new BlackboardPostEvent(moduleDataEvent); + eventPublishingExecutor.submit(new PublishEventTask(event, moduleEventPublisher)); } /** @@ -621,7 +799,8 @@ public class IngestManager { * content. */ void fireIngestModuleContentEvent(ModuleContentEvent moduleContentEvent) { - fireIngestEventsThreadPool.submit(new IngestEventPublisher(ingestModuleEventPublisher, IngestModuleEvent.CONTENT_CHANGED, moduleContentEvent, null)); + AutopsyEvent event = new ContentChangedEvent(moduleContentEvent); + eventPublishingExecutor.submit(new PublishEventTask(event, moduleEventPublisher)); } /** @@ -742,13 +921,13 @@ public class IngestManager { /** * Creates and starts an ingest job for a collection of data sources. */ - private final class IngestJobStarter implements Callable { + private final class StartIngestJobTask implements Callable { private final long threadId; private final IngestJob job; private ProgressHandle progress; - IngestJobStarter(long threadId, IngestJob job) { + StartIngestJobTask(long threadId, IngestJob job) { this.threadId = threadId; this.job = job; } @@ -761,7 +940,7 @@ public class IngestManager { return null; } - if (runInteractively) { + if (RuntimeProperties.coreComponentsAreActive()) { final String displayName = NbBundle.getMessage(this.getClass(), "IngestManager.StartIngestJobsTask.run.displayName"); this.progress = ProgressHandleFactory.createHandle(displayName, new Cancellable() { @Override @@ -791,14 +970,14 @@ public class IngestManager { } /** - * A consumer for an ingest task queue. + * Executes ingest jobs by acting as a consumer for an ingest tasks queue. */ - private final class IngestTaskExecuter implements Runnable { + private final class ExecuteIngestJobsTask implements Runnable { private final long threadId; private final IngestTaskQueue tasks; - IngestTaskExecuter(long threadId, IngestTaskQueue tasks) { + ExecuteIngestJobsTask(long threadId, IngestTaskQueue tasks) { this.threadId = threadId; this.tasks = tasks; } @@ -820,43 +999,33 @@ public class IngestManager { } /** - * Fires ingest events to ingest manager property change listeners. + * Publishes ingest events to both local and remote subscribers. */ - private static final class IngestEventPublisher implements Runnable { + private static final class PublishEventTask implements Runnable { - private final PropertyChangeSupport publisher; - private final IngestJobEvent jobEvent; - private final IngestModuleEvent moduleEvent; - private final Object oldValue; - private final Object newValue; + private final AutopsyEvent event; + private final AutopsyEventPublisher publisher; - IngestEventPublisher(PropertyChangeSupport publisher, IngestJobEvent event, Object oldValue, Object newValue) { + /** + * Constructs an object that publishes ingest events to both local and + * remote subscribers. + * + * @param event The event to publish. + * @param publisher The event publisher. + */ + PublishEventTask(AutopsyEvent event, AutopsyEventPublisher publisher) { + this.event = event; this.publisher = publisher; - this.jobEvent = event; - this.moduleEvent = null; - this.oldValue = oldValue; - this.newValue = newValue; - } - - IngestEventPublisher(PropertyChangeSupport publisher, IngestModuleEvent event, Object oldValue, Object newValue) { - this.publisher = publisher; - this.jobEvent = null; - this.moduleEvent = event; - this.oldValue = oldValue; - this.newValue = newValue; } + /** + * @inheritDoc + */ @Override public void run() { - try { - publisher.firePropertyChange((jobEvent != null ? jobEvent.toString() : moduleEvent.toString()), oldValue, newValue); - } catch (Exception e) { - logger.log(Level.SEVERE, "Ingest manager listener threw exception", e); //NON-NLS - MessageNotifyUtil.Notify.show(NbBundle.getMessage(IngestManager.class, "IngestManager.moduleErr"), - NbBundle.getMessage(IngestManager.class, "IngestManager.moduleErr.errListenToUpdates.msg"), - MessageNotifyUtil.MessageType.ERROR); - } + publisher.publish(event); } + } static final class IngestThreadActivitySnapshot { diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestMessageTopComponent.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestMessageTopComponent.java index 0950360682..dfd5adf689 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestMessageTopComponent.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestMessageTopComponent.java @@ -28,7 +28,6 @@ import org.sleuthkit.autopsy.coreutils.Logger; import javax.swing.Action; import javax.swing.BoxLayout; import javax.swing.JOptionPane; -import org.openide.util.ImageUtilities; import org.openide.util.NbBundle; import org.openide.util.Utilities; import org.openide.windows.Mode; @@ -186,12 +185,6 @@ final class IngestMessageTopComponent extends TopComponent { return TopComponent.PERSISTENCE_ALWAYS; } - @Override - public java.awt.Image getIcon() { - return ImageUtilities.loadImage( - "org/sleuthkit/autopsy/ingest/eye-icon.png"); //NON-NLS - } - void writeProperties(java.util.Properties p) { // better to version settings since initial version as advocated at // http://wiki.apidesign.org/wiki/PropertyFiles diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestMessagesAction.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestMessagesAction.java index a55ef94ebb..64f0d02b46 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestMessagesAction.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestMessagesAction.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011 Basis Technology Corp. + * Copyright 2011-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,14 +23,6 @@ import java.awt.event.ActionEvent; import javax.swing.AbstractAction; import org.openide.util.actions.Presenter; -//@ActionID(category = "File", -//id = "org.sleuthkit.autopsy.ingest.IngestMessagesAction") -//@ActionRegistration(iconBase = "org/sleuthkit/autopsy/ingest/eye-icon.png", -//displayName = "#CTL_IngestMessagesAction") -//@ActionReferences({ -// @ActionReference(path = "Toolbars/File", position = 575) -//}) -//@Messages("CTL_IngestMessagesAction=Messages") final class IngestMessagesAction extends AbstractAction implements Presenter.Toolbar { @Override diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestMessagesToolbar.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestMessagesToolbar.java index 384cfa8607..5c44c49cbf 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestMessagesToolbar.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestMessagesToolbar.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011 Basis Technology Corp. + * Copyright 2011-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,32 +26,26 @@ import java.awt.Graphics; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import javax.swing.JButton; - import org.openide.util.NbBundle; import org.openide.windows.Mode; import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.core.RuntimeProperties; /** - * Toolbar for Ingest - * + * Tool bar for an ingest messages button that allows a user to open the ingest + * messages inbox top component. */ class IngestMessagesToolbar extends javax.swing.JPanel { private IngestMessagesButton ingestMessagesButton = new IngestMessagesButton(); private static volatile IngestMessagesToolbar instance; - /** - * Creates new form IngestMessagesToolbar - */ private IngestMessagesToolbar() { initComponents(); customizeComponents(); } - /** - * @return the default instance IngestMessagesToolbar - */ public static IngestMessagesToolbar getDefault() { if (instance == null) { synchronized (IngestMessagesToolbar.class) { @@ -101,17 +95,8 @@ class IngestMessagesToolbar extends javax.swing.JPanel { ingestMessagesButton.setMaximumSize(new java.awt.Dimension(38, 24)); ingestMessagesButton.setMinimumSize(new java.awt.Dimension(38, 24)); ingestMessagesButton.setPreferredSize(new java.awt.Dimension(38, 24)); - ingestMessagesButton.addActionListener(new java.awt.event.ActionListener() { - @Override - public void actionPerformed(java.awt.event.ActionEvent evt) { - EventQueue.invokeLater(new Runnable() { - @Override - public void run() { - showIngestMessages(); - } - }); - - } + ingestMessagesButton.addActionListener((java.awt.event.ActionEvent evt) -> { + EventQueue.invokeLater(this::showIngestMessages); }); this.add(ingestMessagesButton, BorderLayout.CENTER); } @@ -143,12 +128,9 @@ class IngestMessagesToolbar extends javax.swing.JPanel { } }); - Case.addPropertyChangeListener(new PropertyChangeListener() { - @Override - public void propertyChange(PropertyChangeEvent evt) { - if (evt.getPropertyName().equals(Case.Events.CURRENT_CASE.toString())) { - setEnabled(evt.getNewValue() != null); - } + Case.addPropertyChangeListener((PropertyChangeEvent evt) -> { + if (evt.getPropertyName().equals(Case.Events.CURRENT_CASE.toString())) { + setEnabled(evt.getNewValue() != null && RuntimeProperties.coreComponentsAreActive()); } }); } diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestMonitor.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestMonitor.java index 9d69b6c70f..91da8d183a 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestMonitor.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestMonitor.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2012 Basis Technology Corp. + * Copyright 2011-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,184 +21,205 @@ package org.sleuthkit.autopsy.ingest; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; import java.io.File; import java.io.IOException; import java.util.logging.FileHandler; import java.util.logging.Level; import java.util.logging.SimpleFormatter; - import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; import javax.swing.Timer; -import org.openide.util.Exceptions; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.PlatformUtil; +import org.sleuthkit.autopsy.events.AutopsyEvent; /** - * Monitor health of the system and stop ingest if necessary + * Monitors disk space and memory and cancels ingest if disk space runs low. + *

+ * Note: This should be a singleton and currrently is used as such, with the + * only instance residing in the IngestManager class. */ public final class IngestMonitor { public static final int DISK_FREE_SPACE_UNKNOWN = -1; private static final int INITIAL_INTERVAL_MS = 60000; //1 min. + private static final int MAX_LOG_FILES = 3; + private static final java.util.logging.Logger MONITOR_LOGGER = java.util.logging.Logger.getLogger("monitor"); //NON-NLS private final Logger logger = Logger.getLogger(IngestMonitor.class.getName()); private Timer timer; - private static final java.util.logging.Logger MONITOR_LOGGER = java.util.logging.Logger.getLogger("monitor"); //NON-NLS - private MonitorAction monitor; + private MonitorTimerAction timerAction; + /** + * Constructs an object that monitors disk space and memory and cancels + * ingest if disk space runs low. + */ IngestMonitor() { - - //setup the custom memory logger + /* + * Setup a separate memory usage logger. + */ try { - final int MAX_LOG_FILES = 3; - FileHandler monitorLogHandler = new FileHandler(PlatformUtil.getUserDirectory().getAbsolutePath() + "/var/log/monitor.log", //NON-NLS - 0, MAX_LOG_FILES); + FileHandler monitorLogHandler = new FileHandler(PlatformUtil.getUserDirectory().getAbsolutePath() + "/var/log/monitor.log", 0, MAX_LOG_FILES); //NON-NLS monitorLogHandler.setFormatter(new SimpleFormatter()); monitorLogHandler.setEncoding(PlatformUtil.getLogFileEncoding()); - MONITOR_LOGGER.addHandler(monitorLogHandler); - //do not forward to the parent autopsy logger MONITOR_LOGGER.setUseParentHandlers(false); - } catch (IOException ex) { - Exceptions.printStackTrace(ex); - } catch (SecurityException ex) { - Exceptions.printStackTrace(ex); + MONITOR_LOGGER.addHandler(monitorLogHandler); + } catch (IOException | SecurityException ex) { + logger.log(Level.SEVERE, "Failed to create memory usage logger", ex); } - } /** - * Start the monitor + * Starts the ingest timerAction. */ void start() { - monitor = new MonitorAction(); - timer = new Timer(INITIAL_INTERVAL_MS, monitor); + timerAction = new MonitorTimerAction(); + timer = new Timer(INITIAL_INTERVAL_MS, timerAction); timer.start(); } /** - * Stop the monitor + * Stops the ingest timerAction. */ void stop() { - if (timer != null) { + if (null != timer) { timer.stop(); } } /** - * Check if the monitor is running + * Checks whether or not the ingest timerAction is running * - * @return true if the monitor is running, false otherwise + * @return True or false */ boolean isRunning() { - return timer != null && timer.isRunning(); + return (null != timer && timer.isRunning()); } /** - * Get free space in bytes of the drive where case dir resides + * Gets the free space, in bytes, of the drive where the case folder for the + * current case resides. * - * @return free space in bytes or -1 if could not be determined. + * @return Free space in bytes or -1 if free sapce could not be determined. */ long getFreeSpace() { try { - return monitor.getFreeSpace(); + return timerAction.getFreeSpace(); } catch (SecurityException e) { logger.log(Level.WARNING, "Error checking for free disk space on ingest data drive", e); //NON-NLS return DISK_FREE_SPACE_UNKNOWN; } } - //TODO add support to monitor multiple drives, e.g. user dir drive in addition to Case drive - private class MonitorAction implements ActionListener { + /** + * An action that is called every time the ingest monitor's timer expires. + * It does the actual monitoring. + */ + private class MonitorTimerAction implements ActionListener { - private final static long MIN_FREE_DISK_SPACE = 100L * 1024 * 1024; //100MB - private File root = new File(File.separator); //default, root dir where autopsy runs + private final static long MIN_FREE_DISK_SPACE = 100L * 1024 * 1024; // 100MB + private File root; - MonitorAction() { - //find drive where case is located - setMonitorDir(); - - //update monitor dir if case changed - Case.addPropertyChangeListener(new PropertyChangeListener() { - @Override - public void propertyChange(PropertyChangeEvent evt) { - String changed = evt.getPropertyName(); - Object newValue = evt.getNewValue(); - - if (changed.equals(Case.Events.CURRENT_CASE.toString())) { - if (newValue != null) { - setMonitorDir(); + MonitorTimerAction() { + findRootDirectoryForCurrentCase(); + Case.addEventSubscriber(Case.Events.CURRENT_CASE.toString(), (PropertyChangeEvent evt) -> { + if (evt instanceof AutopsyEvent) { + AutopsyEvent event = (AutopsyEvent) evt; + if (AutopsyEvent.SourceType.LOCAL == event.getSourceType() && event.getPropertyName().equals(Case.Events.CURRENT_CASE.toString())) { + /* + * The new value of the event will be non-null if a new + * case has been opened. + */ + if (null != evt.getNewValue()) { + findRootDirectoryForCurrentCase((Case) evt.getNewValue()); } - } - } }); - } - private void setMonitorDir() { - String caseDir = Case.getCurrentCase().getCaseDirectory(); - File curDir = new File(caseDir); - File tempF = null; - while ((tempF = curDir.getParentFile()) != null) { - curDir = tempF; + /** + * Determines the root directory of the case folder for the current case + * and sets it as the directory to monitor. + */ + private void findRootDirectoryForCurrentCase() { + try { + Case currentCase = Case.getCurrentCase(); + findRootDirectoryForCurrentCase(currentCase); + } catch (IllegalStateException unused) { + /* + * Case.getCurrentCase() throws IllegalStateException when there + * is no case. + */ + root = new File(File.separator); + logMonitoredRootDirectory(); + } + } + + /** + * Determines the root directory of the case folder for the current case + * and sets it as the directory to monitor. + * + * @param currentCase The current case. + */ + private void findRootDirectoryForCurrentCase(Case currentCase) { + File curDir = new File(currentCase.getCaseDirectory()); + File parentDir = curDir.getParentFile(); + while (null != parentDir) { + curDir = parentDir; + parentDir = curDir.getParentFile(); } root = curDir; - logger.log(Level.INFO, "Monitoring disk space of case root: " + curDir.getAbsolutePath()); //NON-NLS + logMonitoredRootDirectory(); + } + + /** + * Writes an info message to the Autopsy log identifying the root + * directory being monitored. + */ + private void logMonitoredRootDirectory() { + logger.log(Level.INFO, "Monitoring disk space of {0}", root.getAbsolutePath()); //NON-NLS } @Override public void actionPerformed(ActionEvent e) { + /* + * Skip monitoring if ingest is not running. + */ final IngestManager manager = IngestManager.getInstance(); - - //runs checks only if ingest is running if (manager.isIngestRunning() == false) { return; } - monitorMemory(); + logMemoryUsage(); - if (checkDiskSpace() == false) { - //stop ingest if running - final String diskPath = root.getAbsolutePath(); - MONITOR_LOGGER.log(Level.SEVERE, "Stopping ingest due to low disk space on disk {0}", diskPath); //NON-NLS - logger.log(Level.SEVERE, "Stopping ingest due to low disk space on disk {0}", diskPath); //NON-NLS + if (!enoughDiskSpace()) { + /* + * Shut down ingest by cancelling all ingest jobs. + */ manager.cancelAllIngestJobs(); + String diskPath = root.getAbsolutePath(); IngestServices.getInstance().postMessage(IngestMessage.createManagerErrorMessage( NbBundle.getMessage(this.getClass(), "IngestMonitor.mgrErrMsg.lowDiskSpace.title", diskPath), NbBundle.getMessage(this.getClass(), "IngestMonitor.mgrErrMsg.lowDiskSpace.msg", diskPath))); + MONITOR_LOGGER.log(Level.SEVERE, "Stopping ingest due to low disk space on {0}", diskPath); //NON-NLS + logger.log(Level.SEVERE, "Stopping ingest due to low disk space on {0}", diskPath); //NON-NLS } } /** - * Get free space in bytes of the drive where case dir resides, or -1 if - * unknown - * - * @return free space in bytes + * Writes current message usage to the memory usage log. */ - private long getFreeSpace() throws SecurityException { - final long freeSpace = root.getFreeSpace(); - - if (freeSpace == 0) { - //check if network drive, some network filesystems always return 0 - final String monitoredPath = root.getAbsolutePath(); - if (monitoredPath.startsWith("\\\\") || monitoredPath.startsWith("//")) { - return DISK_FREE_SPACE_UNKNOWN; - - } - } - - return freeSpace; - + private void logMemoryUsage() { + MONITOR_LOGGER.log(Level.INFO, PlatformUtil.getAllMemUsageInfo()); } /** - * check disk space and see if enough to process/continue ingest + * Determines whether there is enough disk space to continue running + * ingest. * * @return true if OK, false otherwise */ - private boolean checkDiskSpace() { + private boolean enoughDiskSpace() { long freeSpace; try { freeSpace = getFreeSpace(); @@ -210,16 +231,30 @@ public final class IngestMonitor { if (freeSpace == DISK_FREE_SPACE_UNKNOWN) { return true; } else { - //logger.log(Level.INFO, "Checking free disk apce: " + freeSpace + " need: " + Long.toString(MIN_FREE_DISK_SPACE)); return freeSpace > MIN_FREE_DISK_SPACE; } } /** - * Monitor memory usage and print to memory log + * Get free space in bytes of the drive where case dir resides, or -1 if + * unknown + * + * @return free space in bytes */ - private void monitorMemory() { - MONITOR_LOGGER.log(Level.INFO, PlatformUtil.getAllMemUsageInfo()); + private long getFreeSpace() throws SecurityException { + final long freeSpace = root.getFreeSpace(); + if (0 == freeSpace) { + /* + * Check for a network drive, some network filesystems always + * return zero. + */ + final String monitoredPath = root.getAbsolutePath(); + if (monitoredPath.startsWith("\\\\") || monitoredPath.startsWith("//")) { + return DISK_FREE_SPACE_UNKNOWN; + } + } + return freeSpace; } } + } diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestProgressSnapshotDialog.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestProgressSnapshotDialog.java index 7e0d655acf..17c1b56e58 100755 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestProgressSnapshotDialog.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestProgressSnapshotDialog.java @@ -24,9 +24,12 @@ import java.awt.Dialog; import java.awt.Dimension; import java.awt.Toolkit; import java.awt.Window; +import java.awt.event.KeyEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; +import javax.swing.JComponent; import javax.swing.JDialog; +import javax.swing.KeyStroke; import org.openide.util.NbBundle; import org.openide.windows.WindowManager; @@ -43,6 +46,7 @@ public final class IngestProgressSnapshotDialog extends JDialog { */ public IngestProgressSnapshotDialog() { this((Window) WindowManager.getDefault().getMainWindow(), false); + setLocationRelativeTo(WindowManager.getDefault().getMainWindow()); } /** @@ -78,6 +82,9 @@ public final class IngestProgressSnapshotDialog extends JDialog { int w = this.getSize().width; int h = this.getSize().height; setLocation((screenDimension.width - w) / 2, (screenDimension.height - h) / 2); + this.getRootPane().registerKeyboardAction(e -> { + this.dispose(); + }, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); add(new IngestProgressSnapshotPanel(this)); pack(); setResizable(false); diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestTasksScheduler.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestTasksScheduler.java index c1f93832c4..766891ae86 100755 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestTasksScheduler.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestTasksScheduler.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2012-2014 Basis Technology Corp. + * Copyright 2012-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -152,13 +152,15 @@ final class IngestTasksScheduler { * full tasks queue and is interrupted. */ synchronized void scheduleIngestTasks(DataSourceIngestJob job) { - // Scheduling of both a data source ingest task and file ingest tasks - // for a job must be an atomic operation. Otherwise, the data source - // task might be completed before the file tasks are scheduled, - // resulting in a potential false positive when another thread checks - // whether or not all the tasks for the job are completed. - this.scheduleDataSourceIngestTask(job); - this.scheduleFileIngestTasks(job); + if (!job.isCancelled()) { + // Scheduling of both a data source ingest task and file ingest tasks + // for a job must be an atomic operation. Otherwise, the data source + // task might be completed before the file tasks are scheduled, + // resulting in a potential false positive when another thread checks + // whether or not all the tasks for the job are completed. + this.scheduleDataSourceIngestTask(job); + this.scheduleFileIngestTasks(job); + } } /** @@ -167,17 +169,19 @@ final class IngestTasksScheduler { * @param job The job for which the tasks are to be scheduled. */ synchronized void scheduleDataSourceIngestTask(DataSourceIngestJob job) { - DataSourceIngestTask task = new DataSourceIngestTask(job); - this.tasksInProgress.add(task); - try { - this.pendingDataSourceTasks.put(task); - } catch (InterruptedException ex) { - /** - * The current thread was interrupted while blocked on a full queue. - * Discard the task and reset the interrupted flag. - */ - this.tasksInProgress.remove(task); - Thread.currentThread().interrupt(); + if (!job.isCancelled()) { + DataSourceIngestTask task = new DataSourceIngestTask(job); + this.tasksInProgress.add(task); + try { + this.pendingDataSourceTasks.put(task); + } catch (InterruptedException ex) { + /** + * The current thread was interrupted while blocked on a full + * queue. Discard the task and reset the interrupted flag. + */ + this.tasksInProgress.remove(task); + Thread.currentThread().interrupt(); + } } } @@ -187,17 +191,19 @@ final class IngestTasksScheduler { * @param job The job for which the tasks are to be scheduled. */ synchronized void scheduleFileIngestTasks(DataSourceIngestJob job) { - // Get the top level files for the data source associated with this job - // and add them to the root directories priority queue. - List topLevelFiles = getTopLevelFiles(job.getDataSource()); - for (AbstractFile firstLevelFile : topLevelFiles) { - FileIngestTask task = new FileIngestTask(job, firstLevelFile); - if (IngestTasksScheduler.shouldEnqueueFileTask(task)) { - this.tasksInProgress.add(task); - this.rootDirectoryTasks.add(task); + if (!job.isCancelled()) { + // Get the top level files for the data source associated with this job + // and add them to the root directories priority queue. + List topLevelFiles = getTopLevelFiles(job.getDataSource()); + for (AbstractFile firstLevelFile : topLevelFiles) { + FileIngestTask task = new FileIngestTask(job, firstLevelFile); + if (IngestTasksScheduler.shouldEnqueueFileTask(task)) { + this.tasksInProgress.add(task); + this.rootDirectoryTasks.add(task); + } } + shuffleFileTaskQueues(); } - shuffleFileTaskQueues(); } /** @@ -207,10 +213,12 @@ final class IngestTasksScheduler { * @param file The file to be associated with the task. */ synchronized void scheduleFileIngestTask(DataSourceIngestJob job, AbstractFile file) { - FileIngestTask task = new FileIngestTask(job, file); - if (IngestTasksScheduler.shouldEnqueueFileTask(task)) { - this.tasksInProgress.add(task); - addToPendingFileTasksQueue(task); + if (!job.isCancelled()) { + FileIngestTask task = new FileIngestTask(job, file); + if (IngestTasksScheduler.shouldEnqueueFileTask(task)) { + this.tasksInProgress.add(task); + addToPendingFileTasksQueue(task); + } } } @@ -242,19 +250,27 @@ final class IngestTasksScheduler { } /** - * Clears the task scheduling queues for an ingest job, but does nothing - * about tasks that have already been taken by ingest threads. Those tasks - * will be flushed out when the ingest threads call back with their task - * completed notifications. + * Clears the "upstream" task scheduling queues for an ingest job, but does + * nothing about tasks that have already been shuffled into the concurrently + * accessed blocking queues shared with the ingest threads. Note that tasks + * in the "downstream" queues or already taken by the ingest threads will be + * flushed out when the ingest threads call back with their task completed + * notifications. * * @param job The job for which the tasks are to to canceled. */ synchronized void cancelPendingTasksForIngestJob(DataSourceIngestJob job) { + /** + * This code should not flush the blocking queues that are concurrently + * accessed by the ingest threads. This is because the "lock striping" + * and "weakly consistent" iterators of these collections make it so + * that this code could have a different view of the queues than the + * ingest threads. It does clean out the directory level tasks before + * they are exploded into file tasks. + */ long jobId = job.getId(); this.removeTasksForJob(this.rootDirectoryTasks, jobId); this.removeTasksForJob(this.directoryTasks, jobId); - this.removeTasksForJob(this.pendingFileTasks, jobId); - this.removeTasksForJob(this.pendingDataSourceTasks, jobId); this.shuffleFileTaskQueues(); } @@ -465,7 +481,7 @@ final class IngestTasksScheduler { * @param taskQueue The queue from which to remove the tasks. * @param jobId The id of the job for which the tasks are to be removed. */ - private void removeTasksForJob(Collection taskQueue, long jobId) { + synchronized private void removeTasksForJob(Collection taskQueue, long jobId) { Iterator iterator = taskQueue.iterator(); while (iterator.hasNext()) { IngestTask task = iterator.next(); diff --git a/Core/src/org/sleuthkit/autopsy/ingest/ModuleContentEvent.java b/Core/src/org/sleuthkit/autopsy/ingest/ModuleContentEvent.java index 2bd4f834bd..00ed2a84f3 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/ModuleContentEvent.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/ModuleContentEvent.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013 Basis Technology Corp. + * Copyright 2013-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,28 +22,41 @@ import javax.swing.event.ChangeEvent; import org.sleuthkit.datamodel.Content; /** - * Event data that are fired off by ingest modules when they changed or added - * new content. + * Event data that is published when content is added to case. */ public class ModuleContentEvent extends ChangeEvent { - private String moduleName; + private String moduleName = ""; /** - * Create a new event passing content that has changed + * Constructs event data that is published when content is added to case. * - * @param content + * @param content A reference to the new content. */ public ModuleContentEvent(Content content) { super(content); } /** - * get module name that changed the content and fired the event + * Constructs event data that is published when content is added to case. * - * @return + * @param content A reference to the new content. + * @param moduleName The name of the module that added the content. + * @param content + */ + public ModuleContentEvent(String moduleName, Content content) { + super(content); + this.moduleName = moduleName; + } + + /** + * Gets the name of the module that added the content, if the module name + * has been provided. + * + * @return The module name as a string. May be empty. */ public String getModuleName() { return moduleName; } + } diff --git a/Core/src/org/sleuthkit/autopsy/events/BlackBoardArtifactTagDeletedEvent.java b/Core/src/org/sleuthkit/autopsy/ingest/ProcTerminationCode.java similarity index 50% rename from Core/src/org/sleuthkit/autopsy/events/BlackBoardArtifactTagDeletedEvent.java rename to Core/src/org/sleuthkit/autopsy/ingest/ProcTerminationCode.java index b2901ac5dc..d9a005942d 100644 --- a/Core/src/org/sleuthkit/autopsy/events/BlackBoardArtifactTagDeletedEvent.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/ProcTerminationCode.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2015 Basis Technology Corp. + * Copyright 2014 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,17 +16,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.events; - -import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.datamodel.BlackboardArtifactTag; +package org.sleuthkit.autopsy.ingest; /** - * + * Process termination codes. */ -public class BlackBoardArtifactTagDeletedEvent extends TagDeletedEvent { +public enum ProcTerminationCode { - public BlackBoardArtifactTagDeletedEvent(BlackboardArtifactTag oldValue) { - super(Case.Events.BLACKBOARD_ARTIFACT_TAG_DELETED.toString(), oldValue); - } -} + /** + * Property name that indicates the process was not terminated or canceled. + */ + NONE, + /** + * Property name that indicates the process was terminated due to exceeding + * maximum allowable run time. + */ + TIME_OUT, + /** + * Property name that indicates the process was canceled. + */ + CANCELATION, + /** + * Property name that indicates the process was terminated via custom + * implementation of ProcessTerminator. + */ + CUSTOM; +}; diff --git a/Core/src/org/sleuthkit/autopsy/ingest/RunIngestAction.java b/Core/src/org/sleuthkit/autopsy/ingest/RunIngestAction.java new file mode 100755 index 0000000000..151548a9b8 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/ingest/RunIngestAction.java @@ -0,0 +1,90 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-2015 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.ingest; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import javax.swing.JMenuItem; +import org.openide.awt.ActionID; +import org.openide.awt.ActionRegistration; +import org.openide.util.HelpCtx; +import org.openide.util.NbBundle; +import org.openide.util.NbBundle.Messages; +import org.openide.util.actions.CallableSystemAction; +import org.openide.util.actions.Presenter; + +@ActionID( + category = "Tools", + id = "org.sleuthkit.autopsy.ingest.RunIngestAction" +) +@ActionRegistration( + displayName = "#CTL_RunIngestAction", + lazy = false +) +@Messages("CTL_RunIngestAction=Run Ingest") +public final class RunIngestAction extends CallableSystemAction implements Presenter.Menu, ActionListener { + + private static final long serialVersionUID = 1L; + + static public RunIngestAction getInstance() { + return new RunIngestAction(); + } + + /** + * @inheritDoc + */ + @Override + public void performAction() { + getMenuPresenter(); + } + + /** + * @inheritDoc + */ + @Override + public String getName() { + return NbBundle.getMessage(RunIngestAction.class, "RunIngestModulesMenu.getName.text"); + } + + /** + * @inheritDoc + */ + @Override + public HelpCtx getHelpCtx() { + return HelpCtx.DEFAULT_HELP; + } + + /** + * @inheritDoc + */ + @Override + public JMenuItem getMenuPresenter() { + JMenuItem sublist = new RunIngestSubMenu(); + sublist.setVisible(true); + return sublist; + } + + /** + * @inheritDoc + */ + @Override + public void actionPerformed(ActionEvent e) { + performAction(); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/ingest/RunIngestModulesAction.java b/Core/src/org/sleuthkit/autopsy/ingest/RunIngestModulesAction.java new file mode 100755 index 0000000000..50a2319529 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/ingest/RunIngestModulesAction.java @@ -0,0 +1,52 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-2015 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.ingest; + +import java.awt.event.ActionEvent; +import java.util.Collections; +import javax.swing.AbstractAction; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.Image; + +/** + * This class is used to add the action to the run ingest modules menu item. + * When the image is pressed, it should open the wizard for ingest modules. + */ +final class RunIngestModulesAction extends AbstractAction { + + Image image; + + /** + * the constructor + */ + public RunIngestModulesAction(Image image) { + this.image = image; + } + + /** + * Runs the ingest modules wizard on the image. + * + * @param e the action event + */ + @Override + public void actionPerformed(ActionEvent e) { + final RunIngestModulesDialog ingestDialog = new RunIngestModulesDialog(Collections.singletonList(image)); + ingestDialog.display(); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/ingest/RunIngestModulesDialog.java b/Core/src/org/sleuthkit/autopsy/ingest/RunIngestModulesDialog.java index 4b81506603..da491fd22a 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/RunIngestModulesDialog.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/RunIngestModulesDialog.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-2014 Basis Technology Corp. + * Copyright 2013-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/Core/src/org/sleuthkit/autopsy/ingest/RunIngestSubMenu.java b/Core/src/org/sleuthkit/autopsy/ingest/RunIngestSubMenu.java new file mode 100755 index 0000000000..f4db5d47e8 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/ingest/RunIngestSubMenu.java @@ -0,0 +1,99 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-2015 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.ingest; + +import java.util.ArrayList; +import java.util.List; +import javax.swing.JComponent; +import javax.swing.JMenuItem; +import org.openide.awt.DynamicMenuContent; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.datamodel.Image; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * This class is used to populate the list of open images to run ingest on them + */ +final class RunIngestSubMenu extends JMenuItem implements DynamicMenuContent { + + /** + * Creates main menu/popup menu items. It's called each time a popup menu + * is constructed and just once for the main menu. + * Main menu updates happen through the synchMenuPresenters() method. + * + * @return + */ + @Override + public JComponent[] getMenuPresenters() { + List images = new ArrayList<>(); + + try { + SleuthkitCase sk = Case.getCurrentCase().getSleuthkitCase(); + images = sk.getImages(); + } catch (IllegalStateException ex) { + // No open Cases, create a disabled empty menu + return getEmpty(); + } catch (TskCoreException e) { + System.out.println("Exception getting images: " + e.getMessage()); + } + JComponent[] comps = new JComponent[images.size()]; + + // Add Images to the component list + for (int i = 0; i < images.size(); i++) { + String action = images.get(i).getName(); + JMenuItem menuItem = new JMenuItem(action); + menuItem.setActionCommand(action.toUpperCase()); + menuItem.addActionListener(new RunIngestModulesAction(images.get(i))); + comps[i] = menuItem; + } + // If no images are open, create a disabled empty menu + if (images.isEmpty()) { + return getEmpty(); + } + return comps; + } + + // returns a disabled empty menu + private JComponent[] getEmpty() { + JComponent[] comps = new JComponent[1]; + JMenuItem emptyMenu = new JMenuItem(NbBundle.getMessage(RunIngestSubMenu.class, "RunIngestSubMenu.menuItem.empty")); + comps[0] = emptyMenu; + comps[0].setEnabled(false); + return comps; + } + + /** + * Updates main menu presenters. This method is called only by the main menu + * processing. + * + * @param jcs the previously used menu items returned by previous call to + * getMenuPresenters() or synchMenuPresenters() + * + * @return menu a new set of items to show in menu. Can be either an updated + * old set of instances or a completely new one. + */ + @Override + public JComponent[] synchMenuPresenters(JComponent[] jcs) { + return getMenuPresenters(); + } + + +} diff --git a/Core/src/org/sleuthkit/autopsy/ingest/events/BlackboardPostEvent.java b/Core/src/org/sleuthkit/autopsy/ingest/events/BlackboardPostEvent.java new file mode 100644 index 0000000000..e8d2b7ad63 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/ingest/events/BlackboardPostEvent.java @@ -0,0 +1,126 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.ingest.events; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.logging.Level; +import java.util.stream.Collectors; +import javax.annotation.concurrent.Immutable; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.events.AutopsyEvent; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.autopsy.ingest.ModuleDataEvent; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Event published when new data is posted to the blackboard of a case. The + * "old" value is a legacy ModuleDataEvent object. The "new" value is null. + */ +public final class BlackboardPostEvent extends AutopsyEvent implements Serializable { + + private static final long serialVersionUID = 1L; + private static final Logger logger = Logger.getLogger(BlackboardPostEvent.class.getName()); + private transient ModuleDataEvent eventData; + + /** + * Constructs an event to be published when new content is added to a case + * or there is a change a recorded attribute of existing content. + * + * @param contentEvent A ModuleDataEvent object containing the data + * associated with the blackboard post. + */ + public BlackboardPostEvent(ModuleDataEvent eventData) { + /** + * Putting a serializable data holding object into oldValue to allow for + * lazy loading of the ModuleDataEvent object for remote events. This + * bypasses the issues related to the serialization and de-serialization + * of BlackboardArtifact objects when the event is published over a + * network. + */ + super( + IngestManager.IngestModuleEvent.DATA_ADDED.toString(), + new SerializableEventData(eventData.getModuleName(), eventData.getArtifactType(), eventData.getArtifacts() != null + ? eventData.getArtifacts() + .stream() + .map(BlackboardArtifact::getArtifactID) + .collect(Collectors.toList()) : Collections.emptyList()), + null + ); + this.eventData = eventData; + } + + /** + * Gets the legacy ModuleDataEvent object associated with this event. + * + * @return The ModuleDataEvent. + */ + @Override + public Object getOldValue() { + /** + * The eventData field is set in the constructor, but it is transient so + * it will become null when the event is serialized for publication over + * a network. Doing a lazy load of the ModuleDataEvent object bypasses + * the issues related to the serialization and de-serialization of + * BlackboardArtifact objects and may also save database round trips + * from other nodes since subscribers to this event are often not + * interested in the event data. + */ + if (null != eventData) { + return eventData; + } + try { + SerializableEventData data = (SerializableEventData) super.getOldValue(); + Collection artifacts = new ArrayList<>(); + for (Long id : data.artifactIds) { + artifacts.add(Case.getCurrentCase().getSleuthkitCase().getBlackboardArtifact(id)); + } + eventData = new ModuleDataEvent(data.moduleName, data.artifactType, !artifacts.isEmpty() ? artifacts : null); + return eventData; + } catch (IllegalStateException | TskCoreException ex) { + logger.log(Level.SEVERE, "Error doing lazy load for remote event", ex); + return null; + } + } + + /** + * Data holder class. + */ + @Immutable + private static final class SerializableEventData implements Serializable { + + private static final long serialVersionUID = 1L; + private final String moduleName; + private ARTIFACT_TYPE artifactType; + private Collection artifactIds; + + private SerializableEventData(String moduleName, ARTIFACT_TYPE artifactType, Collection artifactIds) { + this.moduleName = moduleName; + this.artifactType = artifactType; + this.artifactIds = new ArrayList<>(artifactIds); + } + + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/ingest/events/ContentChangedEvent.java b/Core/src/org/sleuthkit/autopsy/ingest/events/ContentChangedEvent.java new file mode 100644 index 0000000000..3ff37f4be1 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/ingest/events/ContentChangedEvent.java @@ -0,0 +1,113 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.ingest.events; + +import java.io.Serializable; +import java.util.logging.Level; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.events.AutopsyEvent; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.autopsy.ingest.ModuleContentEvent; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Event published when new content is added to a case or there is a change a + * recorded attribute of existing content. For example, a content changed event + * should be published when an analysis (ingest) module adds an extracted or + * carved file to a case. The "old" value is a legacy ModuleContentEvent object. + * The "new" value is null. + */ +public final class ContentChangedEvent extends AutopsyEvent implements Serializable { + + private static final long serialVersionUID = 1L; + private static final Logger logger = Logger.getLogger(ContentChangedEvent.class.getName()); + private transient ModuleContentEvent eventData; + + /** + * Constructs a event to be published when new content is added to a case or + * there is a change a recorded attribute of existing content. + * + * @param contentEvent A ModuleContentEvent object containing the data + * associated with the content addition or change. + */ + public ContentChangedEvent(ModuleContentEvent eventData) { + /** + * Putting a serializable data holding object into newValue to allow for + * lazy loading of the ModuleContent object. This bypasses the issues + * related to the serialization and de-serialization of Content objects + * when the event is published over a network. + */ + super( + IngestManager.IngestModuleEvent.CONTENT_CHANGED.toString(), + new SerializableEventData(eventData.getModuleName(), ((Content) eventData.getSource()).getId()), + null + ); + } + + /** + * Gets the legacy ModuleContentEvent object associated with this event. + * Note that the content object that was added or changed can be accessed + * via the getSource() method of the ModuleContentEvent. + * + * @return The ModuleContentEvent. + */ + @Override + public Object getOldValue() { + /** + * The eventData field is set in the constructor, but it is transient so + * it will become null when the event is serialized for publication over + * a network. Doing a lazy load of the ModuleContentEvent object + * bypasses the issues related to the serialization and de-serialization + * of Content objects and may also save database round trips from other + * nodes since subscribers to this event are often not interested in the + * event data. + */ + if (null != eventData) { + return eventData; + } + try { + SerializableEventData data = (SerializableEventData) super.getOldValue(); + Content content = Case.getCurrentCase().getSleuthkitCase().getContentById(data.contentId); + eventData = new ModuleContentEvent(data.moduleName, content); + return eventData; + } catch (IllegalStateException | TskCoreException ex) { + logger.log(Level.SEVERE, "Error doing lazy load for remote event", ex); + return null; + } + } + + /** + * Data holder class. + */ + private static final class SerializableEventData implements Serializable { + + private static final long serialVersionUID = 1L; + private final String moduleName; + private final long contentId; + + private SerializableEventData(String moduleName, long contentId) { + this.moduleName = moduleName; + this.contentId = contentId; + } + + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/ingest/events/DataSourceAnalysisCompletedEvent.java b/Core/src/org/sleuthkit/autopsy/ingest/events/DataSourceAnalysisCompletedEvent.java new file mode 100644 index 0000000000..0df66fabb6 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/ingest/events/DataSourceAnalysisCompletedEvent.java @@ -0,0 +1,66 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.ingest.events; + +import java.io.Serializable; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.datamodel.Content; + +/** + * Event published when analysis (ingest) of a data source included in an ingest + * job is completed. + */ +public class DataSourceAnalysisCompletedEvent extends DataSourceAnalysisEvent implements Serializable { + + /** + * The reason why the analysis of the data source completed. + */ + public enum Reason { + + ANALYSIS_COMPLETED, + ANALYSIS_CANCELLED + } + + private static final long serialVersionUID = 1L; + private final Reason reason; + + /** + * Constructs an event published when analysis (ingest) of a data source + * included in an ingest job is completed. + * + * @param ingestJobId The identifier of the ingest job, specific + * to this node. + * @param dataSourceIngestJobId The identifier of the data source ingest + * job,specific to this node. + * @param dataSource The data source. + * @param reason The reason analysis completed. + */ + public DataSourceAnalysisCompletedEvent(long ingestJobId, long dataSourceIngestJobId, Content dataSource, Reason reason) { + super(IngestManager.IngestJobEvent.DATA_SOURCE_ANALYSIS_COMPLETED, ingestJobId, dataSourceIngestJobId, dataSource); + this.reason = reason; + } + + /** + * Gets the reason why the analysis of the data source completed. + */ + public Reason getResult() { + return reason; + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/ingest/events/DataSourceAnalysisEvent.java b/Core/src/org/sleuthkit/autopsy/ingest/events/DataSourceAnalysisEvent.java new file mode 100644 index 0000000000..f401285044 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/ingest/events/DataSourceAnalysisEvent.java @@ -0,0 +1,107 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.ingest.events; + +import java.io.Serializable; +import java.util.logging.Level; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.events.AutopsyEvent; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * A base class for events published in connection with the analysis (ingest) of + * a data source. + */ +public abstract class DataSourceAnalysisEvent extends AutopsyEvent implements Serializable { + + private static final long serialVersionUID = 1L; + private static final Logger logger = Logger.getLogger(DataSourceAnalysisEvent.class.getName()); + private final long ingestJobId; + private final long dataSourceIngestJobId; + private transient Content dataSource; + + /** + * Constructs an instance of the base class for events published in + * connection with the analysis (ingest) of a data source. + * + * @param eventType The event string for the subtype. + * @param ingestJobId The identifier of the ingest job, specific + * to this node. + * @param dataSourceIngestJobId The identifier of the data source ingest + * job,specific to this node. + * @param dataSource The data source. + */ + public DataSourceAnalysisEvent(IngestManager.IngestJobEvent eventType, long ingestJobId, long dataSourceIngestJobId, Content dataSource) { + super(eventType.toString(), null, null); + this.ingestJobId = ingestJobId; + this.dataSourceIngestJobId = dataSourceIngestJobId; + this.dataSource = dataSource; + } + + /** + * Gets the id of the ingest job of which the analysis of this data source + * is a part. + * + * @return The id. + */ + public long getIngestJobId() { + return ingestJobId; + } + + /** + * Gets the id of the data source ingest job of which the analysis of this + * data source is a part. + * + * @return The id. + */ + public long getDataSourceIngestJobId() { + return dataSourceIngestJobId; + } + + /** + * Gets the data source associated with this event. + * + * @return The data source. + */ + public Content getDataSource() { + /** + * The dataSource field is set in the constructor, but it is transient + * so it will become null when the event is serialized for publication + * over a network. Doing a lazy load of the Content object bypasses the + * issues related to the serialization and de-serialization of Content + * objects and may also save database round trips from other nodes since + * subscribers to this event are often not interested in the event data. + */ + if (null != dataSource) { + return dataSource; + } + try { + long id = (Long) super.getNewValue(); + dataSource = Case.getCurrentCase().getSleuthkitCase().getContentById(id); + return dataSource; + } catch (IllegalStateException | TskCoreException ex) { + logger.log(Level.SEVERE, "Error doing lazy load for remote event", ex); + return null; + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/ingest/events/DataSourceAnalysisStartedEvent.java b/Core/src/org/sleuthkit/autopsy/ingest/events/DataSourceAnalysisStartedEvent.java new file mode 100644 index 0000000000..6975120eae --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/ingest/events/DataSourceAnalysisStartedEvent.java @@ -0,0 +1,47 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.ingest.events; + +import java.io.Serializable; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.datamodel.Content; + +/** + * Event published when analysis (ingest) of a data source included in an ingest + * job is started. + */ +public class DataSourceAnalysisStartedEvent extends DataSourceAnalysisEvent implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * Constructs an event published when analysis (ingest) of a data source + * included in an ingest job is started. + * + * @param ingestJobId The identifier of the ingest job, specific + * to this node. + * @param dataSourceIngestJobId The identifier of the data source ingest + * job,specific to this node. + * @param dataSource The data source. + */ + public DataSourceAnalysisStartedEvent(long ingestJobId, long dataSourceIngestJobId, Content dataSource) { + super(IngestManager.IngestJobEvent.DATA_SOURCE_ANALYSIS_STARTED, ingestJobId, dataSourceIngestJobId, dataSource); + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/ingest/events/FileAnalyzedEvent.java b/Core/src/org/sleuthkit/autopsy/ingest/events/FileAnalyzedEvent.java new file mode 100644 index 0000000000..ca629bf261 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/ingest/events/FileAnalyzedEvent.java @@ -0,0 +1,87 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.ingest.events; + +import java.io.Serializable; +import java.util.logging.Level; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.events.AutopsyEvent; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Event published when the analysis (ingest) of a file is completed. The "old" + * value is the Autopsy object id of the file. The "new" value is an + * AbstractFile object for that id. + */ +public final class FileAnalyzedEvent extends AutopsyEvent implements Serializable { + + private static final long serialVersionUID = 1L; + private static final Logger logger = Logger.getLogger(ContentChangedEvent.class.getName()); + private transient AbstractFile file; + + /** + * Constructs an event that can be used to indicate that the analysis + * (ingest) of a file is completed. + * + * @param file The file for which analysis is completed. + */ + public FileAnalyzedEvent(AbstractFile file) { + /** + * Putting null into newValue to allow for lazy loading of the + * AbstractFile object. This bypasses the issues related to the + * serialization and de-serialization of AbstractFile objects when the + * event is published over a network. + */ + super(IngestManager.IngestModuleEvent.FILE_DONE.toString(), file.getId(), null); + this.file = file; + } + + /** + * Gets the file for which analysis is completed. + * + * @return The file. + */ + @Override + public Object getNewValue() { + /** + * The file field is set in the constructor, but it is transient so it + * will become null when the event is serialized for publication over a + * network. Doing a lazy load of the AbstractFile object bypasses the + * issues related to the serialization and de-serialization of + * AbstractFile objects and may also save database round trips from + * other nodes since subscribers to this event are often not interested + * in the event data. + */ + if (null != file) { + return file; + } + try { + long id = (Long) super.getOldValue(); + file = Case.getCurrentCase().getSleuthkitCase().getAbstractFileById(id); + return file; + } catch (IllegalStateException | TskCoreException ex) { + logger.log(Level.SEVERE, "Error doing lazy load for remote event", ex); + return null; + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/keywordsearchservice/KeywordSearchService.java b/Core/src/org/sleuthkit/autopsy/keywordsearchservice/KeywordSearchService.java index 028b9358ce..69b7c57f4f 100644 --- a/Core/src/org/sleuthkit/autopsy/keywordsearchservice/KeywordSearchService.java +++ b/Core/src/org/sleuthkit/autopsy/keywordsearchservice/KeywordSearchService.java @@ -36,4 +36,17 @@ public interface KeywordSearchService extends Closeable { * @throws org.sleuthkit.datamodel.TskCoreException */ public void indexArtifact(BlackboardArtifact artifact) throws TskCoreException; -} + + /** + * Checks if we can communicate with the KeywordSearchService using the + * passed-in host and port. Closes the connection upon exit. Throws if it + * cannot communicate. + * + * @param host the remote hostname or IP address of the server + * @param port the remote port of the server + * + * @throws KeywordSearchServiceException + */ + public void tryConnect(String host, int port) throws KeywordSearchServiceException; + + } diff --git a/Core/src/org/sleuthkit/autopsy/keywordsearchservice/KeywordSearchServiceException.java b/Core/src/org/sleuthkit/autopsy/keywordsearchservice/KeywordSearchServiceException.java new file mode 100755 index 0000000000..975ac059f1 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/keywordsearchservice/KeywordSearchServiceException.java @@ -0,0 +1,53 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.keywordsearchservice; + +/** + * Provides a system exception for the Keyword Search package. + */ +public class KeywordSearchServiceException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Constructs a new exception with null as its message. + */ + public KeywordSearchServiceException() { + super(); + } + + /** + * Constructs a new exception with the specified message. + * + * @param message The message. + */ + public KeywordSearchServiceException(String message) { + super(message); + } + + /** + * Constructs a new exception with the specified message and cause. + * + * @param message The message. + * @param cause The cause. + */ + public KeywordSearchServiceException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/modules/android/BrowserLocationAnalyzer.java b/Core/src/org/sleuthkit/autopsy/modules/android/BrowserLocationAnalyzer.java index 66abd62142..ab5ed97cd4 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/android/BrowserLocationAnalyzer.java +++ b/Core/src/org/sleuthkit/autopsy/modules/android/BrowserLocationAnalyzer.java @@ -87,7 +87,7 @@ class BrowserLocationAnalyzer { try { resultSet = statement.executeQuery( - "Select timestamp, latitude, longitude, accuracy FROM CachedPosition;"); //NON-NLS + "SELECT timestamp, latitude, longitude, accuracy FROM CachedPosition;"); //NON-NLS while (resultSet.next()) { Long timestamp = Long.valueOf(resultSet.getString("timestamp")) / 1000; //NON-NLS diff --git a/Core/src/org/sleuthkit/autopsy/modules/android/GoogleMapLocationAnalyzer.java b/Core/src/org/sleuthkit/autopsy/modules/android/GoogleMapLocationAnalyzer.java index e65ca11c3f..3265b9359c 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/android/GoogleMapLocationAnalyzer.java +++ b/Core/src/org/sleuthkit/autopsy/modules/android/GoogleMapLocationAnalyzer.java @@ -87,7 +87,7 @@ class GoogleMapLocationAnalyzer { try { resultSet = statement.executeQuery( - "Select time,dest_lat,dest_lng,dest_title,dest_address,source_lat,source_lng FROM destination_history;"); //NON-NLS + "SELECT time,dest_lat,dest_lng,dest_title,dest_address,source_lat,source_lng FROM destination_history;"); //NON-NLS while (resultSet.next()) { Long time = Long.valueOf(resultSet.getString("time")) / 1000; //NON-NLS diff --git a/Core/src/org/sleuthkit/autopsy/modules/android/TangoMessageAnalyzer.java b/Core/src/org/sleuthkit/autopsy/modules/android/TangoMessageAnalyzer.java index 522ca5a286..9a3c460ef6 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/android/TangoMessageAnalyzer.java +++ b/Core/src/org/sleuthkit/autopsy/modules/android/TangoMessageAnalyzer.java @@ -84,7 +84,7 @@ class TangoMessageAnalyzer { try { resultSet = statement.executeQuery( - "Select conv_id, create_time,direction,payload FROM messages ORDER BY create_time DESC;"); //NON-NLS + "SELECT conv_id, create_time,direction,payload FROM messages ORDER BY create_time DESC;"); //NON-NLS String conv_id; // seems to wrap around the message found in payload after decoding from base-64 String direction; // 1 incoming, 2 outgoing diff --git a/Core/src/org/sleuthkit/autopsy/modules/android/TextMessageAnalyzer.java b/Core/src/org/sleuthkit/autopsy/modules/android/TextMessageAnalyzer.java index 165c193bc5..48a177fe10 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/android/TextMessageAnalyzer.java +++ b/Core/src/org/sleuthkit/autopsy/modules/android/TextMessageAnalyzer.java @@ -84,7 +84,7 @@ class TextMessageAnalyzer { try { resultSet = statement.executeQuery( - "Select address,date,read,type,subject,body FROM sms;"); //NON-NLS + "SELECT address,date,read,type,subject,body FROM sms;"); //NON-NLS String address; // may be phone number, or other addresses diff --git a/Core/src/org/sleuthkit/autopsy/modules/e01verify/E01VerifyIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/e01verify/E01VerifyIngestModule.java index d0a813b61b..c4b1df80b2 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/e01verify/E01VerifyIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/modules/e01verify/E01VerifyIngestModule.java @@ -23,6 +23,7 @@ import java.security.NoSuchAlgorithmException; import java.util.logging.Level; import javax.xml.bind.DatatypeConverter; import org.openide.util.NbBundle; +import org.python.bouncycastle.util.Arrays; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.ingest.DataSourceIngestModule; import org.sleuthkit.autopsy.ingest.DataSourceIngestModuleProgress; @@ -48,7 +49,6 @@ public class E01VerifyIngestModule implements DataSourceIngestModule { private MessageDigest messageDigest; private boolean verified = false; - private boolean skipped = false; private String calculatedHash = ""; private String storedHash = ""; private IngestJobContext context; @@ -75,8 +75,10 @@ public class E01VerifyIngestModule implements DataSourceIngestModule { @Override public ProcessResult process(Content dataSource, DataSourceIngestModuleProgress statusHelper) { String imgName = dataSource.getName(); + + // Skip non-images if (!(dataSource instanceof Image)) { - logger.log(Level.INFO, "Skipping disk image image {0}", imgName); //NON-NLS + logger.log(Level.INFO, "Skipping non-image {0}", imgName); //NON-NLS services.postMessage(IngestMessage.createMessage(MessageType.INFO, E01VerifierModuleFactory.getModuleName(), NbBundle.getMessage(this.getClass(), "EwfVerifyIngestModule.process.skipNonEwf", @@ -95,16 +97,17 @@ public class E01VerifyIngestModule implements DataSourceIngestModule { return ProcessResult.OK; } - if ((img.getMd5() != null) && !img.getMd5().isEmpty()) { - storedHash = img.getMd5().toLowerCase(); - logger.log(Level.INFO, "Hash value stored in {0}: {1}", new Object[]{imgName, storedHash}); //NON-NLS - } else { + // Report an error for null or empty MD5 + if ((img.getMd5() == null) || img.getMd5().isEmpty()) { services.postMessage(IngestMessage.createMessage(MessageType.ERROR, E01VerifierModuleFactory.getModuleName(), NbBundle.getMessage(this.getClass(), "EwfVerifyIngestModule.process.noStoredHash", imgName))); return ProcessResult.ERROR; } + + storedHash = img.getMd5().toLowerCase(); + logger.log(Level.INFO, "Hash value stored in {0}: {1}", new Object[]{imgName, storedHash}); //NON-NLS logger.log(Level.INFO, "Starting hash verification of {0}", img.getName()); //NON-NLS services.postMessage(IngestMessage.createMessage(MessageType.INFO, E01VerifierModuleFactory.getModuleName(), @@ -126,11 +129,12 @@ public class E01VerifyIngestModule implements DataSourceIngestModule { long chunkSize = 64 * img.getSsize(); chunkSize = (chunkSize == 0) ? DEFAULT_CHUNK_SIZE : chunkSize; - int totalChunks = (int) Math.ceil(size / chunkSize); + // Casting to double to capture decimals + int totalChunks = (int) Math.ceil((double)size / (double)chunkSize); logger.log(Level.INFO, "Total chunks = {0}", totalChunks); //NON-NLS int read; - byte[] data; + byte[] data = new byte[(int) chunkSize]; statusHelper.switchToDeterminate(totalChunks); // Read in byte size chunks and update the hash value with the data. @@ -138,7 +142,6 @@ public class E01VerifyIngestModule implements DataSourceIngestModule { if (context.dataSourceIngestIsCancelled()) { return ProcessResult.OK; } - data = new byte[(int) chunkSize]; try { read = img.read(data, i * chunkSize, chunkSize); } catch (TskCoreException ex) { @@ -148,7 +151,14 @@ public class E01VerifyIngestModule implements DataSourceIngestModule { logger.log(Level.SEVERE, msg, ex); return ProcessResult.ERROR; } - messageDigest.update(data); + + // Only update with the read bytes. + if(read == chunkSize) { + messageDigest.update(data); + } else { + byte[] subData = Arrays.copyOfRange(data, 0, read); + messageDigest.update(subData); + } statusHelper.progress(i); } diff --git a/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/EmbeddedFileExtractorIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/EmbeddedFileExtractorIngestModule.java index 12cb37fecb..7e0d220f11 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/EmbeddedFileExtractorIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/EmbeddedFileExtractorIngestModule.java @@ -25,7 +25,6 @@ import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.ingest.IngestServices; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.autopsy.ingest.FileIngestModule; -import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; import org.sleuthkit.autopsy.ingest.IngestModule.ProcessResult; import org.sleuthkit.autopsy.ingest.IngestJobContext; @@ -69,8 +68,8 @@ public final class EmbeddedFileExtractorIngestModule implements FileIngestModule final Case currentCase = Case.getCurrentCase(); - moduleDirRelative = Case.getModulesOutputDirRelPath() + File.separator + EmbeddedFileExtractorModuleFactory.getModuleName(); //relative to the case, to store in db - moduleDirAbsolute = currentCase.getModulesOutputDirAbsPath() + File.separator + EmbeddedFileExtractorModuleFactory.getModuleName(); //absolute, to extract to + moduleDirRelative = currentCase.getModuleOutputDirectoryRelativePath() + File.separator + EmbeddedFileExtractorModuleFactory.getModuleName(); //relative to the case, to store in db + moduleDirAbsolute = currentCase.getModuleDirectory() + File.separator + EmbeddedFileExtractorModuleFactory.getModuleName(); //absolute, to extract to // initialize the folder where the embedded files are extracted. File extractionDirectory = new File(moduleDirAbsolute); @@ -103,7 +102,7 @@ public final class EmbeddedFileExtractorIngestModule implements FileIngestModule return ProcessResult.OK; } - // skip unknown files + // skip known files if (abstractFile.getKnown().equals(TskData.FileKnown.KNOWN)) { return ProcessResult.OK; } @@ -116,7 +115,7 @@ public final class EmbeddedFileExtractorIngestModule implements FileIngestModule return ProcessResult.OK; } - // call the archive extractor if archiveextraction flag is set. + // call the archive extractor if archiveExtraction flag is set. if (this.archivextraction) { archiveExtractor.unpack(abstractFile); } diff --git a/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/SevenZipExtractor.java b/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/SevenZipExtractor.java old mode 100755 new mode 100644 index ffcdc5b34b..e38887de7b --- a/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/SevenZipExtractor.java +++ b/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/SevenZipExtractor.java @@ -45,6 +45,7 @@ import org.netbeans.api.progress.ProgressHandleFactory; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.services.FileManager; +import org.sleuthkit.autopsy.coreutils.FileUtil; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.ingest.IngestJobContext; import org.sleuthkit.autopsy.ingest.IngestMessage; @@ -251,7 +252,7 @@ class SevenZipExtractor { } if (detectedFormat == null) { - logger.log(Level.WARNING, "Could not detect format for file: " + archiveFile); //NON-NLS + logger.log(Level.WARNING, "Could not detect format for file: {0}", archiveFile); //NON-NLS // if we don't have attribute info then use file extension String extension = archiveFile.getNameExtension(); @@ -407,6 +408,7 @@ class SevenZipExtractor { logger.log(Level.WARNING, msg); } + archiveFilePath = FileUtil.escapeFileName(archiveFilePath); ++itemNumber; logger.log(Level.INFO, "Extracted item path: {0}", pathInArchive); //NON-NLS @@ -476,8 +478,8 @@ class SevenZipExtractor { localFile.getParentFile().mkdirs(); try { localFile.createNewFile(); - } catch (IOException ex) { - logger.log(Level.SEVERE, "Error creating extracted file: " + localFile.getAbsolutePath(), ex); //NON-NLS + } catch (IOException e) { + logger.log(Level.SEVERE, "Error creating extracted file: " + localFile.getAbsolutePath(), e); //NON-NLS } } } catch (SecurityException e) { @@ -546,7 +548,7 @@ class SevenZipExtractor { } } catch (SevenZipException ex) { - logger.log(Level.SEVERE, "Error unpacking file: " + archiveFile, ex); //NON-NLS + logger.log(Level.SEVERE, "Error unpacking file: {0}", archiveFile); //NON-NLS //inbox message // print a message if the file is allocated @@ -901,7 +903,7 @@ class SevenZipExtractor { UnpackedNode(String fileName, UnpackedNode parent) { this.fileName = fileName; this.parent = parent; - //this.localRelPath = parent.localRelPath + File.separator + fileName; + this.localRelPath = parent.localRelPath + File.separator + fileName; //new child derived file will be set by unpack() method parent.children.add(this); diff --git a/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/FileExtMismatchOptionsPanelController.java b/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/FileExtMismatchOptionsPanelController.java index 376590f259..8a4638ac28 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/FileExtMismatchOptionsPanelController.java +++ b/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/FileExtMismatchOptionsPanelController.java @@ -18,7 +18,7 @@ import org.sleuthkit.autopsy.coreutils.Logger; @OptionsPanelController.TopLevelRegistration( categoryName = "#OptionsCategory_Name_FileExtMismatchOptions", iconBase = "org/sleuthkit/autopsy/modules/fileextmismatch/options-icon.png", - position = 4, + position = 5, keywords = "#OptionsCategory_FileExtMismatch", keywordsCategory = "KeywordSearchOptions") public final class FileExtMismatchOptionsPanelController extends OptionsPanelController { diff --git a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileType.java b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileType.java index a1358edb4f..80ca8a13df 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileType.java +++ b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileType.java @@ -176,6 +176,9 @@ class FileType { * @return True or false. */ boolean containedIn(final AbstractFile file) { + if (file.getSize() < (offset + signatureBytes.length)) { + return false; /// too small, can't contain this signature + } try { byte[] buffer = new byte[signatureBytes.length]; int bytesRead = file.read(buffer, offset, signatureBytes.length); diff --git a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeIdOptionsPanelController.java b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeIdOptionsPanelController.java index e7abd1f9d8..d4b7270f45 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeIdOptionsPanelController.java +++ b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeIdOptionsPanelController.java @@ -17,7 +17,7 @@ import org.openide.util.Lookup; iconBase = "org/sleuthkit/autopsy/modules/filetypeid/user-defined-file-types-settings.png", keywords = "#OptionsCategory_Keywords_FileTypeId", keywordsCategory = "FileTypeId", - position = 5 + position = 6 ) // moved messages to Bundle.properties //@org.openide.util.NbBundle.Messages({"OptionsCategory_Name_FileTypeId=FileTypeId", "OptionsCategory_Keywords_FileTypeId=FileTypeId"}) diff --git a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/UserDefinedFileTypesManager.java b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/UserDefinedFileTypesManager.java index 2fe4efea3c..79de53d2f3 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/UserDefinedFileTypesManager.java +++ b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/UserDefinedFileTypesManager.java @@ -181,9 +181,13 @@ final class UserDefinedFileTypesManager { */ private void loadPredefinedFileTypes() throws UserDefinedFileTypesException { try { - FileType fileType = new FileType("text/xml", new Signature(" sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -39,6 +39,7 @@ import org.openide.util.Exceptions; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.corecomponents.OptionsPanel; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.events.AutopsyEvent; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.autopsy.modules.hashdatabase.HashDbManager.HashDb; @@ -57,8 +58,8 @@ public final class HashLookupSettingsPanel extends IngestModuleGlobalSettingsPan .getMessage(HashLookupSettingsPanel.class, "HashDbConfigPanel.errorGettingPathText"); private static final String ERROR_GETTING_INDEX_STATUS_TEXT = NbBundle .getMessage(HashLookupSettingsPanel.class, "HashDbConfigPanel.errorGettingIndexStatusText"); - private HashDbManager hashSetManager = HashDbManager.getInstance(); - private HashSetTableModel hashSetTableModel = new HashSetTableModel(); + private final HashDbManager hashSetManager = HashDbManager.getInstance(); + private final HashSetTableModel hashSetTableModel = new HashSetTableModel(); public HashLookupSettingsPanel() { initComponents(); @@ -70,7 +71,7 @@ public final class HashLookupSettingsPanel extends IngestModuleGlobalSettingsPan IngestManager.getInstance().addIngestJobEventListener(new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { - if (isIngestJobEvent(evt)) { + if (isLocalIngestJobEvent(evt)) { EventQueue.invokeLater(new Runnable() { @Override public void run() { @@ -236,10 +237,17 @@ public final class HashLookupSettingsPanel extends IngestModuleGlobalSettingsPan return shortenedPath; } - private boolean isIngestJobEvent(PropertyChangeEvent evt) { - return evt.getPropertyName().equals(IngestManager.IngestJobEvent.STARTED.toString()) - || evt.getPropertyName().equals(IngestManager.IngestJobEvent.COMPLETED.toString()) - || evt.getPropertyName().equals(IngestManager.IngestJobEvent.CANCELLED.toString()); + private boolean isLocalIngestJobEvent(PropertyChangeEvent evt) { + if (evt instanceof AutopsyEvent) { + AutopsyEvent event = (AutopsyEvent) evt; + if (event.getSourceType() == AutopsyEvent.SourceType.LOCAL) { + String eventType = event.getPropertyName(); + return (eventType.equals(IngestManager.IngestJobEvent.STARTED.toString()) + || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString()) + || eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString())); + } + } + return false; } @Override diff --git a/Core/src/org/sleuthkit/autopsy/modules/iOS/ContactAnalyzer.java b/Core/src/org/sleuthkit/autopsy/modules/iOS/ContactAnalyzer.java index 7a6e3c6967..0ba1125ef3 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/iOS/ContactAnalyzer.java +++ b/Core/src/org/sleuthkit/autopsy/modules/iOS/ContactAnalyzer.java @@ -56,8 +56,8 @@ class ContactAnalyzer { List absFiles; try { SleuthkitCase skCase = Case.getCurrentCase().getSleuthkitCase(); - absFiles = skCase.findAllFilesWhere("name LIKE '%call_history%' "); //NON-NLS //get exact file names - if (absFiles.isEmpty()) { //asdfkjasfakljsdfhlaksdjfhasdlkjf + absFiles = skCase.findAllFilesWhere("LOWER(name) LIKE LOWER('%call_history%') "); //NON-NLS //get exact file names + if (absFiles.isEmpty()) { return; } for (AbstractFile AF : absFiles) { diff --git a/Core/src/org/sleuthkit/autopsy/modules/iOS/TextMessageAnalyzer.java b/Core/src/org/sleuthkit/autopsy/modules/iOS/TextMessageAnalyzer.java index fecaf6b744..a6090bc7e4 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/iOS/TextMessageAnalyzer.java +++ b/Core/src/org/sleuthkit/autopsy/modules/iOS/TextMessageAnalyzer.java @@ -92,7 +92,7 @@ class TextMessageAnalyzer { try { resultSet = statement.executeQuery( - "Select address,date,type,subject,body FROM sms;"); //NON-NLS + "SELECT address,date,type,subject,body FROM sms;"); //NON-NLS BlackboardArtifact bba; String address; // may be phone number, or other addresses diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesIdentifierIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesIdentifierIngestModule.java index a834865591..1e68abcdcc 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesIdentifierIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesIdentifierIngestModule.java @@ -126,11 +126,13 @@ final class FilesIdentifierIngestModule implements FileIngestModule { */ @Override public void shutDown() { - if (refCounter.decrementAndGet(this.context.getJobId()) == 0) { - // Shutting down the last instance of this module for this ingest - // job, so discard the interesting file sets definitions snapshot - // for the job. - FilesIdentifierIngestModule.interestingFileSetsByJob.remove(this.context.getJobId()); + if (context != null) { + if (refCounter.decrementAndGet(this.context.getJobId()) == 0) { + // Shutting down the last instance of this module for this ingest + // job, so discard the interesting file sets definitions snapshot + // for the job. + FilesIdentifierIngestModule.interestingFileSetsByJob.remove(this.context.getJobId()); + } } } } diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/InterestingItemDefsOptionsPanelController.java b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/InterestingItemDefsOptionsPanelController.java index b23ecb8dc0..127d0c45af 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/InterestingItemDefsOptionsPanelController.java +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/InterestingItemDefsOptionsPanelController.java @@ -31,7 +31,7 @@ import org.openide.util.Lookup; iconBase = "org/sleuthkit/autopsy/images/interesting_item_32x32.png", keywords = "#OptionsCategory_Keywords_InterestingItemDefinitions", keywordsCategory = "InterestingItemDefinitions", - position = 6 + position = 7 ) public final class InterestingItemDefsOptionsPanelController extends OptionsPanelController { diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/InterestingItemDefsPanel.form b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/InterestingItemDefsPanel.form index 8407ea5753..b5d9288bee 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/InterestingItemDefsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/InterestingItemDefsPanel.form @@ -496,6 +496,7 @@ + diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/InterestingItemDefsPanel.java b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/InterestingItemDefsPanel.java index 9cc03a068f..b4e6d5ad89 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/InterestingItemDefsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/InterestingItemDefsPanel.java @@ -543,9 +543,10 @@ final class InterestingItemDefsPanel extends IngestModuleGlobalSettingsPanel imp org.openide.awt.Mnemonics.setLocalizedText(rulePathFilterRegexCheckBox, org.openide.util.NbBundle.getMessage(InterestingItemDefsPanel.class, "InterestingItemDefsPanel.rulePathFilterRegexCheckBox.text")); // NOI18N rulePathFilterRegexCheckBox.setEnabled(false); + jTextArea1.setEditable(false); jTextArea1.setBackground(new java.awt.Color(240, 240, 240)); jTextArea1.setColumns(20); - jTextArea1.setFont(jTextArea1.getFont().deriveFont(Font.PLAIN, 11)); // NON-NLS + jTextArea1.setFont(new java.awt.Font("Tahoma", 0, 11)); // NOI18N jTextArea1.setLineWrap(true); jTextArea1.setRows(3); jTextArea1.setText(org.openide.util.NbBundle.getMessage(InterestingItemDefsPanel.class, "InterestingItemDefsPanel.jTextArea1.text")); // NOI18N diff --git a/Core/src/org/sleuthkit/autopsy/modules/photoreccarver/Bundle.properties b/Core/src/org/sleuthkit/autopsy/modules/photoreccarver/Bundle.properties index df5844903c..ed1afb9be9 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/photoreccarver/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/modules/photoreccarver/Bundle.properties @@ -10,6 +10,7 @@ unsupportedOS.message=PhotoRec Module is supported only on Windows platforms missingExecutable.message=Unable to locate PhotoRec executable. cannotRunExecutable.message=Unable to execute PhotoRec cannotCreateOutputDir.message=Unable to create output directory: {0} +PhotoRecIngestModule.nonHostnameUNCPathUsed=Photorec cannot operate on a UNC path containing IP addresses PhotoRecIngestModule.processTerminated=PhotoRec Carver ingest module was terminated due to exceeding max allowable run time when scanning PhotoRecIngestModule.moduleError=PhotoRec Carver Module Error PhotoRecIngestModule.UnableToCarve=Unable to carve file: {0} diff --git a/Core/src/org/sleuthkit/autopsy/modules/photoreccarver/PhotoRecCarverFileIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/photoreccarver/PhotoRecCarverFileIngestModule.java index 4a54c2eb7a..b923c4294a 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/photoreccarver/PhotoRecCarverFileIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/modules/photoreccarver/PhotoRecCarverFileIngestModule.java @@ -40,19 +40,13 @@ import org.openide.modules.InstalledFileLocator; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.ExecUtil; -import org.sleuthkit.autopsy.coreutils.FileUtil; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; -import org.sleuthkit.autopsy.coreutils.PlatformUtil; import org.sleuthkit.autopsy.datamodel.ContentUtils; import org.sleuthkit.autopsy.ingest.FileIngestModule; -import org.sleuthkit.autopsy.ingest.FileIngestModuleProcessTerminator; import org.sleuthkit.autopsy.ingest.IngestJobContext; import org.sleuthkit.autopsy.ingest.IngestMessage; import org.sleuthkit.autopsy.ingest.IngestModule; import org.sleuthkit.autopsy.ingest.IngestModuleReferenceCounter; -import org.sleuthkit.autopsy.ingest.IngestServices; -import org.sleuthkit.autopsy.ingest.ModuleContentEvent; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.Image; @@ -60,6 +54,15 @@ import org.sleuthkit.datamodel.LayoutFile; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; import org.sleuthkit.datamodel.Volume; +import org.sleuthkit.autopsy.coreutils.FileUtil; +import org.sleuthkit.autopsy.coreutils.UNCPathUtilities; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.coreutils.PlatformUtil; +import org.sleuthkit.autopsy.ingest.ProcTerminationCode; +import org.sleuthkit.autopsy.ingest.FileIngestModuleProcessTerminator; +import org.sleuthkit.autopsy.ingest.IngestMonitor; +import org.sleuthkit.autopsy.ingest.IngestServices; +import org.sleuthkit.autopsy.ingest.ModuleContentEvent; /** * A file ingest module that runs the Unallocated Carver executable with @@ -82,6 +85,7 @@ final class PhotoRecCarverFileIngestModule implements FileIngestModule { private Path rootOutputDirPath; private File executableFile; private IngestServices services; + private UNCPathUtilities uncPathUtilities = new UNCPathUtilities(); private long jobId; private static class IngestJobTotals { @@ -123,7 +127,7 @@ final class PhotoRecCarverFileIngestModule implements FileIngestModule { throw new IngestModule.IngestModuleException(NbBundle.getMessage(PhotoRecCarverFileIngestModule.class, "unallocatedSpaceProcessingSettingsError.message")); } - this.rootOutputDirPath = PhotoRecCarverFileIngestModule.createModuleOutputDirectoryForCase(); + this.rootOutputDirPath = createModuleOutputDirectoryForCase(); Path execName = Paths.get(PHOTOREC_DIRECTORY, PHOTOREC_EXECUTABLE); executableFile = locateExecutable(execName.toString()); @@ -180,17 +184,14 @@ final class PhotoRecCarverFileIngestModule implements FileIngestModule { } // Check that we have roughly enough disk space left to complete the operation - long freeDiskSpace = IngestServices.getInstance().getFreeDiskSpace(); - // Some network drives always return -1 for free disk space. + // Some network drives always return -1 for free disk space. // In this case, expect enough space and move on. - - if ((freeDiskSpace != -1) && ((file.getSize() * 1.2) > freeDiskSpace)) { - totals.totalItemsWithErrors.incrementAndGet(); + long freeDiskSpace = IngestServices.getInstance().getFreeDiskSpace(); + if ((freeDiskSpace != IngestMonitor.DISK_FREE_SPACE_UNKNOWN) && ((file.getSize() * 1.2) > freeDiskSpace)) { logger.log(Level.SEVERE, "PhotoRec error processing {0} with {1} Not enough space on primary disk to save unallocated space.", // NON-NLS new Object[]{file.getName(), PhotoRecCarverIngestModuleFactory.getModuleName()}); // NON-NLS - MessageNotifyUtil.Notify.error(NbBundle.getMessage(PhotoRecCarverFileIngestModule.class, "PhotoRecIngestModule.UnableToCarve", file.getName()), - NbBundle.getMessage(PhotoRecCarverFileIngestModule.class, "PhotoRecIngestModule.NotEnoughDiskSpace")); - + MessageNotifyUtil.Notify.error(NbBundle.getMessage(this.getClass(), "PhotoRecIngestModule.UnableToCarve", file.getName()), + NbBundle.getMessage(this.getClass(), "PhotoRecIngestModule.NotEnoughDiskSpace")); return IngestModule.ProcessResult.ERROR; } @@ -219,7 +220,8 @@ final class PhotoRecCarverFileIngestModule implements FileIngestModule { processAndSettings.redirectErrorStream(true); processAndSettings.redirectOutput(Redirect.appendTo(log)); - int exitValue = ExecUtil.execute(processAndSettings, new FileIngestModuleProcessTerminator(this.context)); + FileIngestModuleProcessTerminator terminator = new FileIngestModuleProcessTerminator(this.context, true); + int exitValue = ExecUtil.execute(processAndSettings, terminator); if (this.context.fileIngestIsCancelled() == true) { // if it was cancelled by the user, result is OK @@ -227,6 +229,12 @@ final class PhotoRecCarverFileIngestModule implements FileIngestModule { logger.log(Level.INFO, "PhotoRec cancelled by user"); // NON-NLS MessageNotifyUtil.Notify.info(PhotoRecCarverIngestModuleFactory.getModuleName(), NbBundle.getMessage(PhotoRecCarverFileIngestModule.class, "PhotoRecIngestModule.cancelledByUser")); return IngestModule.ProcessResult.OK; + } else if (terminator.getTerminationCode() == ProcTerminationCode.TIME_OUT) { + cleanup(outputDirPath, tempFilePath); + String msg = NbBundle.getMessage(this.getClass(), "PhotoRecIngestModule.processTerminated") + file.getName(); // NON-NLS + MessageNotifyUtil.Notify.error(NbBundle.getMessage(this.getClass(), "PhotoRecIngestModule.moduleError"), msg); // NON-NLS + logger.log(Level.SEVERE, msg); + return IngestModule.ProcessResult.ERROR; } else if (0 != exitValue) { // if it failed or was cancelled by timeout, result is ERROR cleanup(outputDirPath, tempFilePath); @@ -367,8 +375,15 @@ final class PhotoRecCarverFileIngestModule implements FileIngestModule { * * @throws org.sleuthkit.autopsy.ingest.IngestModule.IngestModuleException */ - synchronized static Path createModuleOutputDirectoryForCase() throws IngestModule.IngestModuleException { - Path path = Paths.get(Case.getCurrentCase().getModulesOutputDirAbsPath(), PhotoRecCarverIngestModuleFactory.getModuleName()); + synchronized Path createModuleOutputDirectoryForCase() throws IngestModule.IngestModuleException { + Path path = Paths.get(Case.getCurrentCase().getModuleDirectory(), PhotoRecCarverIngestModuleFactory.getModuleName()); + if (UNCPathUtilities.isUNC(path)) { + // if the UNC path is using an IP address, convert to hostname + path = uncPathUtilities.ipToHostName(path); + if (path == null) { + throw new IngestModule.IngestModuleException(NbBundle.getMessage(PhotoRecCarverFileIngestModule.class, "PhotoRecIngestModule.nonHostnameUNCPathUsed")); + } + } try { Files.createDirectory(path); } catch (FileAlreadyExistsException ex) { diff --git a/Core/src/org/sleuthkit/autopsy/modules/stix/EvalFileObj.java b/Core/src/org/sleuthkit/autopsy/modules/stix/EvalFileObj.java index e08c6566fc..4bb3733bc1 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/stix/EvalFileObj.java +++ b/Core/src/org/sleuthkit/autopsy/modules/stix/EvalFileObj.java @@ -85,7 +85,7 @@ class EvalFileObj extends EvaluatableObject { if (obj.getFileExtension() != null) { if ((obj.getFileExtension().getCondition() == null) || (obj.getFileExtension().getCondition() == ConditionTypeEnum.EQUALS)) { - String newClause = "name LIKE \'%" + obj.getFileExtension().getValue() + "\'"; //NON-NLS + String newClause = "LOWER(name) LIKE LOWER(\'%" + obj.getFileExtension().getValue() + "\')"; //NON-NLS whereClause = addClause(whereClause, newClause); } else { addWarning( diff --git a/Core/src/org/sleuthkit/autopsy/report/Bundle.properties b/Core/src/org/sleuthkit/autopsy/report/Bundle.properties index c7f862b1fc..5e74024afe 100644 --- a/Core/src/org/sleuthkit/autopsy/report/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/report/Bundle.properties @@ -3,6 +3,7 @@ CTL_ReportWizardAction=Run Report ArtifactSelectionDialog.titleLabel.text=Select which artifacts you would like to report on: ArtifactSelectionDialog.okButton.text=OK ReportVisualPanel1.reportModulesLabel.text=Report Modules: +ReportVisualPanel1.invalidModuleWarning=Encountered an invalid Report Module ({0}) DefaultReportConfigurationPanel.infoLabel.text=This report will be configured on the next screen. ReportVisualPanel2.dataLabel.text=Select which data to report on: ReportVisualPanel2.deselectAllButton.text=Deselect All diff --git a/Core/src/org/sleuthkit/autopsy/report/ReportBodyFile.java b/Core/src/org/sleuthkit/autopsy/report/ReportBodyFile.java index 4e40b15fb6..c495ac2c72 100644 --- a/Core/src/org/sleuthkit/autopsy/report/ReportBodyFile.java +++ b/Core/src/org/sleuthkit/autopsy/report/ReportBodyFile.java @@ -87,7 +87,7 @@ class ReportBodyFile implements GeneralReportModule { + " AND name != '.' AND name != '..'"; //NON-NLS progressPanel.updateStatusLabel(NbBundle.getMessage(this.getClass(), "ReportBodyFile.progress.loading")); - List fs = skCase.findFilesWhere(query); + List fs = skCase.findAllFilesWhere(query); // Check if ingest has finished String ingestwarning = ""; @@ -105,7 +105,7 @@ class ReportBodyFile implements GeneralReportModule { out.write(ingestwarning); // Loop files and write info to report int count = 0; - for (FsContent file : fs) { + for (AbstractFile file : fs) { if (progressPanel.getStatus() == ReportStatus.CANCELED) { break; } @@ -166,7 +166,7 @@ class ReportBodyFile implements GeneralReportModule { logger.log(Level.SEVERE, errorMessage, ex); } } - progressPanel.complete(); + progressPanel.complete(ReportStatus.COMPLETE); } catch (TskCoreException ex) { logger.log(Level.WARNING, "Failed to get the unique path.", ex); //NON-NLS } diff --git a/Core/src/org/sleuthkit/autopsy/report/ReportGenerator.java b/Core/src/org/sleuthkit/autopsy/report/ReportGenerator.java index ccb22c3f55..26f5f2ee3c 100644 --- a/Core/src/org/sleuthkit/autopsy/report/ReportGenerator.java +++ b/Core/src/org/sleuthkit/autopsy/report/ReportGenerator.java @@ -114,7 +114,7 @@ class ReportGenerator { DateFormat dateFormat = new SimpleDateFormat("MM-dd-yyyy-HH-mm-ss"); Date date = new Date(); String dateNoTime = dateFormat.format(date); - this.reportPath = currentCase.getCaseDirectory() + File.separator + REPORTS_DIR + File.separator + currentCase.getName() + " " + dateNoTime + File.separator; + this.reportPath = currentCase.getReportDirectory() + File.separator + currentCase.getName() + " " + dateNoTime + File.separator; this.errorList = new ArrayList(); @@ -151,7 +151,7 @@ class ReportGenerator { if (entry.getValue()) { TableReportModule module = entry.getKey(); String reportFilePath = module.getRelativeFilePath(); - if (reportFilePath != null) { + if (!reportFilePath.isEmpty()) { tableProgress.put(module, panel.addReport(module.getName(), reportPath + reportFilePath)); } else { tableProgress.put(module, panel.addReport(module.getName(), null)); @@ -165,7 +165,7 @@ class ReportGenerator { if (entry.getValue()) { GeneralReportModule module = entry.getKey(); String reportFilePath = module.getRelativeFilePath(); - if (reportFilePath != null) { + if (!reportFilePath.isEmpty()) { generalProgress.put(module, panel.addReport(module.getName(), reportPath + reportFilePath)); } else { generalProgress.put(module, panel.addReport(module.getName(), null)); @@ -179,7 +179,7 @@ class ReportGenerator { if (entry.getValue()) { FileReportModule module = entry.getKey(); String reportFilePath = module.getRelativeFilePath(); - if (reportFilePath != null) { + if (!reportFilePath.isEmpty()) { fileProgress.put(module, panel.addReport(module.getName(), reportPath + reportFilePath)); } else { fileProgress.put(module, panel.addReport(module.getName(), null)); @@ -382,7 +382,7 @@ class ReportGenerator { List absFiles; try { SleuthkitCase skCase = Case.getCurrentCase().getSleuthkitCase(); - absFiles = skCase.findAllFilesWhere("NOT meta_type = " + TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_DIR.getValue()); //NON-NLS + absFiles = skCase.findAllFilesWhere("meta_type != " + TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_DIR.getValue()); //NON-NLS return absFiles; } catch (TskCoreException ex) { MessageNotifyUtil.Notify.show( @@ -914,13 +914,19 @@ class ReportGenerator { // @@@ There is a bug in here. We should use the tags in the below code // so that we only report the lists that we will later provide with real // hits. If no keyord hits are tagged, then we make the page for nothing. + String orderByClause; + if (currentCase.getCaseType() == Case.CaseType.MULTI_USER_CASE) { + orderByClause = "ORDER BY convert_to(att.value_text, 'SQL_ASCII') ASC NULLS FIRST"; //NON-NLS + } else { + orderByClause = "ORDER BY att.value_text ASC"; //NON-NLS + } String keywordListQuery = "SELECT att.value_text AS list " + //NON-NLS "FROM blackboard_attributes AS att, blackboard_artifacts AS art " + //NON-NLS "WHERE att.attribute_type_id = " + ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID() + " " + //NON-NLS "AND art.artifact_type_id = " + ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID() + " " + //NON-NLS "AND att.artifact_id = art.artifact_id " + //NON-NLS - "GROUP BY list"; //NON-NLS + "GROUP BY list " + orderByClause; //NON-NLS try (CaseDbQuery dbQuery = skCase.executeQuery(keywordListQuery)) { ResultSet listsRs = dbQuery.getResultSet(); @@ -947,6 +953,14 @@ class ReportGenerator { return; } + if (currentCase.getCaseType() == Case.CaseType.MULTI_USER_CASE) { + orderByClause = "ORDER BY convert_to(att3.value_text, 'SQL_ASCII') ASC NULLS FIRST, " //NON-NLS + + "convert_to(att1.value_text, 'SQL_ASCII') ASC NULLS FIRST, " //NON-NLS + + "convert_to(f.parent_path, 'SQL_ASCII') ASC NULLS FIRST, " //NON-NLS + + "convert_to(f.name, 'SQL_ASCII') ASC NULLS FIRST"; //NON-NLS + } else { + orderByClause = "ORDER BY att3.value_text ASC, att1.value_text ASC, f.parent_path ASC, f.name ASC"; //NON-NLS + } // Query for keywords, grouped by list String keywordsQuery = "SELECT art.artifact_id, art.obj_id, att1.value_text AS keyword, att2.value_text AS preview, att3.value_text AS list, f.name AS name, f.parent_path AS parent_path " + //NON-NLS @@ -959,7 +973,7 @@ class ReportGenerator { "AND (att2.attribute_type_id = " + ATTRIBUTE_TYPE.TSK_KEYWORD_PREVIEW.getTypeID() + ") " + //NON-NLS "AND (att3.attribute_type_id = " + ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID() + ") " + //NON-NLS "AND (art.artifact_type_id = " + ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID() + ") " + //NON-NLS - "ORDER BY list, keyword, parent_path, name"; //NON-NLS + orderByClause; //NON-NLS try (CaseDbQuery dbQuery = skCase.executeQuery(keywordsQuery)) { ResultSet resultSet = dbQuery.getResultSet(); @@ -1059,13 +1073,19 @@ class ReportGenerator { */ @SuppressWarnings("deprecation") private void writeHashsetHits(List tableModules, String comment, HashSet tagNamesFilter) { + String orderByClause; + if (currentCase.getCaseType() == Case.CaseType.MULTI_USER_CASE) { + orderByClause = "ORDER BY convert_to(att.value_text, 'SQL_ASCII') ASC NULLS FIRST"; //NON-NLS + } else { + orderByClause = "ORDER BY att.value_text ASC"; //NON-NLS + } String hashsetsQuery = "SELECT att.value_text AS list " + //NON-NLS "FROM blackboard_attributes AS att, blackboard_artifacts AS art " + //NON-NLS "WHERE att.attribute_type_id = " + ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID() + " " + //NON-NLS "AND art.artifact_type_id = " + ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID() + " " + //NON-NLS "AND att.artifact_id = art.artifact_id " + //NON-NLS - "GROUP BY list"; //NON-NLS + "GROUP BY list " + orderByClause; //NON-NLS try (CaseDbQuery dbQuery = skCase.executeQuery(hashsetsQuery)) { // Query for hashsets @@ -1088,6 +1108,14 @@ class ReportGenerator { return; } + if (currentCase.getCaseType() == Case.CaseType.MULTI_USER_CASE) { + orderByClause = "ORDER BY convert_to(att.value_text, 'SQL_ASCII') ASC NULLS FIRST, " //NON-NLS + + "convert_to(f.parent_path, 'SQL_ASCII') ASC NULLS FIRST, " //NON-NLS + + "convert_to(f.name, 'SQL_ASCII') ASC NULLS FIRST, " //NON-NLS + + "size ASC NULLS FIRST"; //NON-NLS + } else { + orderByClause = "ORDER BY att.value_text ASC, f.parent_path ASC, f.name ASC, size ASC"; //NON-NLS + } String hashsetHitsQuery = "SELECT art.artifact_id, art.obj_id, att.value_text AS setname, f.name AS name, f.size AS size, f.parent_path AS parent_path " + //NON-NLS "FROM blackboard_artifacts AS art, blackboard_attributes AS att, tsk_files AS f " + //NON-NLS @@ -1095,7 +1123,7 @@ class ReportGenerator { "AND (f.obj_id = art.obj_id) " + //NON-NLS "AND (att.attribute_type_id = " + ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID() + ") " + //NON-NLS "AND (art.artifact_type_id = " + ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID() + ") " + //NON-NLS - "ORDER BY setname, parent_path, name, size"; //NON-NLS + orderByClause; //NON-NLS try (CaseDbQuery dbQuery = skCase.executeQuery(hashsetHitsQuery)) { // Query for hashset hits diff --git a/Core/src/org/sleuthkit/autopsy/report/ReportVisualPanel1.java b/Core/src/org/sleuthkit/autopsy/report/ReportVisualPanel1.java index acd49f029e..9b6acfd092 100644 --- a/Core/src/org/sleuthkit/autopsy/report/ReportVisualPanel1.java +++ b/Core/src/org/sleuthkit/autopsy/report/ReportVisualPanel1.java @@ -27,6 +27,7 @@ import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.logging.Level; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JRadioButton; @@ -34,11 +35,12 @@ import javax.swing.ListCellRenderer; import javax.swing.ListSelectionModel; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; +import org.openide.DialogDisplayer; +import org.openide.NotifyDescriptor; import org.openide.util.Lookup; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.python.JythonModuleLoader; -import org.sleuthkit.autopsy.report.ReportHTML; final class ReportVisualPanel1 extends JPanel implements ListSelectionListener { @@ -64,23 +66,39 @@ final class ReportVisualPanel1 extends JPanel implements ListSelectionListener { // Initialize the list of ReportModules private void initModules() { for (TableReportModule module : Lookup.getDefault().lookupAll(TableReportModule.class)) { - tableModules.add(module); - modules.add(module); + if (moduleIsValid(module)) { + tableModules.add(module); + modules.add(module); + } else { + popupWarning(module); + } } for (GeneralReportModule module : Lookup.getDefault().lookupAll(GeneralReportModule.class)) { - generalModules.add(module); - modules.add(module); + if (moduleIsValid(module)) { + generalModules.add(module); + modules.add(module); + } else { + popupWarning(module); + } } for (GeneralReportModule module : JythonModuleLoader.getGeneralReportModules()) { - generalModules.add(module); - modules.add(module); + if (moduleIsValid(module)) { + generalModules.add(module); + modules.add(module); + } else { + popupWarning(module); + } } for (FileReportModule module : Lookup.getDefault().lookupAll(FileReportModule.class)) { - fileModules.add(module); - modules.add(module); + if (moduleIsValid(module)) { + fileModules.add(module); + modules.add(module); + } else { + popupWarning(module); + } } Collections.sort(modules, new Comparator() { @@ -117,6 +135,20 @@ final class ReportVisualPanel1 extends JPanel implements ListSelectionListener { modulesJList.setSelectedIndex(selectedIndex); } + // Make sure that the report module has a valid non-null name. + private boolean moduleIsValid(ReportModule module) { + return module.getName() != null && !module.getName().isEmpty() + && module.getRelativeFilePath() != null; + } + + private void popupWarning(ReportModule module) { + String moduleClassName = module.getClass().getSimpleName(); + logger.log(Level.WARNING, "Invalid ReportModule: {0}", moduleClassName); // NON_NLS + DialogDisplayer.getDefault().notify(new NotifyDescriptor.Message( + NbBundle.getMessage(ReportVisualPanel1.class, "ReportVisualPanel1.invalidModuleWarning", moduleClassName), + NotifyDescriptor.ERROR_MESSAGE)); + } + @Override public String getName() { return NbBundle.getMessage(this.getClass(), "ReportVisualPanel1.getName.text"); diff --git a/Core/src/org/sleuthkit/autopsy/report/ReportWizardAction.java b/Core/src/org/sleuthkit/autopsy/report/ReportWizardAction.java index 0215d6cf10..231942223a 100644 --- a/Core/src/org/sleuthkit/autopsy/report/ReportWizardAction.java +++ b/Core/src/org/sleuthkit/autopsy/report/ReportWizardAction.java @@ -2,7 +2,7 @@ * * Autopsy Forensic Browser * - * Copyright 2013 Basis Technology Corp. + * Copyright 2013-2015 Basis Technology Corp. * * Copyright 2012 42six Solutions. * Contact: aebadirad 42six com @@ -26,11 +26,8 @@ import java.awt.Component; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; -import java.io.File; import java.text.MessageFormat; import java.util.Map; -import java.util.logging.Level; import javax.swing.ImageIcon; import javax.swing.JButton; import org.openide.DialogDisplayer; @@ -44,20 +41,16 @@ import org.openide.util.NbBundle; import org.openide.util.actions.CallableSystemAction; import org.openide.util.actions.Presenter; import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.core.RuntimeProperties; import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; @ActionID(category = "Tools", id = "org.sleuthkit.autopsy.report.ReportWizardAction") @ActionRegistration(displayName = "#CTL_ReportWizardAction", lazy = false) @ActionReferences(value = { @ActionReference(path = "Menu/Tools", position = 80)}) -// moved into Bundle -//@NbBundle.Messages(value = "CTL_ReportWizardAction=Run Report") public final class ReportWizardAction extends CallableSystemAction implements Presenter.Toolbar, ActionListener { - private static final Logger logger = Logger.getLogger(ReportWizardAction.class.getName()); - - private JButton toolbarButton = new JButton(); + private final JButton toolbarButton = new JButton(); private static final String ACTION_NAME = NbBundle.getMessage(ReportWizardAction.class, "ReportWizardAction.actionName.text"); /** @@ -84,34 +77,15 @@ public final class ReportWizardAction extends CallableSystemAction implements Pr public ReportWizardAction() { setEnabled(false); - Case.addPropertyChangeListener(new PropertyChangeListener() { - @Override - public void propertyChange(PropertyChangeEvent evt) { - if (evt.getPropertyName().equals(Case.Events.CURRENT_CASE.toString())) { - Case newCase = (Case) evt.getNewValue(); - setEnabled(newCase != null); - - // Make the cases' Reoports folder, if it doesn't exist - if (newCase != null) { - boolean exists = (new File(newCase.getCaseDirectory() + File.separator + "Reports")).exists(); - if (!exists) { - boolean reportCreate = (new File(newCase.getCaseDirectory() + File.separator + "Reports")).mkdirs(); - if (!reportCreate) { - logger.log(Level.WARNING, "Could not create Reports directory for case. It does not exist."); //NON-NLS - } - } - } - } + Case.addPropertyChangeListener((PropertyChangeEvent evt) -> { + if (evt.getPropertyName().equals(Case.Events.CURRENT_CASE.toString())) { + Case newCase = (Case) evt.getNewValue(); + setEnabled(newCase != null && RuntimeProperties.coreComponentsAreActive()); } }); // Initialize the Generate Report button - toolbarButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - ReportWizardAction.this.actionPerformed(e); - } - }); + toolbarButton.addActionListener(ReportWizardAction.this::actionPerformed); } @Override @@ -135,9 +109,9 @@ public final class ReportWizardAction extends CallableSystemAction implements Pr } /** - * Returns the toolbar component of this action + * Returns the tool bar component of this action * - * @return component the toolbar button + * @return component the tool bar button */ @Override public Component getToolbarPresenter() { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/FXMLConstructor.java b/Core/src/org/sleuthkit/autopsy/timeline/FXMLConstructor.java index cdf6a38455..0c90af7f27 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/FXMLConstructor.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/FXMLConstructor.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013 Basis Technology Corp. + * Copyright 2013-15 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,10 +21,12 @@ package org.sleuthkit.autopsy.timeline; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.util.logging.Level; import javafx.fxml.FXMLLoader; import javafx.scene.Node; import org.apache.commons.lang3.StringUtils; -import org.openide.util.Exceptions; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; /** * This class supports programmer productivity by abstracting frequently used @@ -34,32 +36,53 @@ import org.openide.util.Exceptions; * at * http://stackoverflow.com/questions/11734885/javafx2-very-poor-performance-when-adding-custom-made-fxmlpanels-to-gridpane. * + * NOTE: As described in the link above above, using FXMLConstructor will be + * inefficient if FXML is used as a template for many similar items. In that use + * case, it is much faster to build the entire hierarchy in Java. This class is + * intended only to remove the boilerplate initialization code when defining a + * relatively static layout + * * TODO: find a way to move this to CoreUtils and remove duplicate verison in * image analyzer */ public class FXMLConstructor { - static public void construct(Node n, String fxmlFileName) { - final String name = "nbres:/" + StringUtils.replace(n.getClass().getPackage().getName(), ".", "/") + "/" + fxmlFileName; // NON-NLS - System.out.println(name); + private static final Logger LOGGER = Logger.getLogger(FXMLConstructor.class.getName()); + + /** + * Load an fxml file and initialize a node with it. Since this manipulates + * the node, it must be called on the JFX thread. + * + * + * @param node a node to initialize from a loaded FXML + * @param fxmlFileName the the file name of the FXML to load, relative to + * the package that the class of node is defined in. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + static public void construct(Node node, String fxmlFileName) { + final String name = "nbres:/" + StringUtils.replace(node.getClass().getPackage().getName(), ".", "/") + "/" + fxmlFileName; // NON-NLS try { FXMLLoader fxmlLoader = new FXMLLoader(new URL(name)); - fxmlLoader.setRoot(n); - fxmlLoader.setController(n); + fxmlLoader.setRoot(node); + fxmlLoader.setController(node); try { fxmlLoader.load(); } catch (IOException exception) { + LOGGER.log(Level.SEVERE, "FXMLConstructor was unable to load FXML, falling back on default Class Loader, and trying again.", exception); try { fxmlLoader.setClassLoader(FXMLLoader.getDefaultClassLoader()); fxmlLoader.load(); } catch (IOException ex) { - Exceptions.printStackTrace(ex); + LOGGER.log(Level.SEVERE, "FXMLConstructor was unable to load FXML, node initialization may not be complete.", ex); } } } catch (MalformedURLException ex) { - Exceptions.printStackTrace(ex); + LOGGER.log(Level.SEVERE, "FXMLConstructor was unable to load FXML, node initialization may not be complete.", ex); } } + + private FXMLConstructor() { + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java index eebf0475bb..e7dd27a635 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014-15 Basis Technology Corp. + * Copyright 2014-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -67,19 +67,20 @@ import static org.sleuthkit.autopsy.casemodule.Case.Events.DATA_SOURCE_ADDED; import org.sleuthkit.autopsy.coreutils.History; import org.sleuthkit.autopsy.coreutils.LoggedTask; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.casemodule.events.BlackBoardArtifactTagAddedEvent; +import org.sleuthkit.autopsy.casemodule.events.BlackBoardArtifactTagDeletedEvent; +import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; +import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.coreutils.ThreadConfined; -import org.sleuthkit.autopsy.events.BlackBoardArtifactTagAddedEvent; -import org.sleuthkit.autopsy.events.BlackBoardArtifactTagDeletedEvent; -import org.sleuthkit.autopsy.events.ContentTagAddedEvent; -import org.sleuthkit.autopsy.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.db.EventsRepository; +import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter; import org.sleuthkit.autopsy.timeline.filters.RootFilter; import org.sleuthkit.autopsy.timeline.filters.TypeFilter; import org.sleuthkit.autopsy.timeline.utils.IntervalUtils; -import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; import org.sleuthkit.datamodel.SleuthkitCase; @@ -136,6 +137,13 @@ public class TimeLineController { private final Case autoCase; + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private final ObservableList quickHideMaskFilters = FXCollections.observableArrayList(); + + public ObservableList getQuickHideFilters() { + return quickHideMaskFilters; + } + /** * @return the autopsy Case assigned to the controller */ @@ -173,7 +181,7 @@ public class TimeLineController { @GuardedBy("this") private final ReadOnlyObjectWrapper viewMode = new ReadOnlyObjectWrapper<>(VisualizationMode.COUNTS); - synchronized public ReadOnlyObjectProperty getViewMode() { + synchronized public ReadOnlyObjectProperty viewModeProperty() { return viewMode.getReadOnlyProperty(); } @@ -256,7 +264,7 @@ public class TimeLineController { InitialZoomState = new ZoomParams(filteredEvents.getSpanningInterval(), EventTypeZoomLevel.BASE_TYPE, filteredEvents.filterProperty().get(), - DescriptionLOD.SHORT); + DescriptionLoD.SHORT); historyManager.advance(InitialZoomState); } @@ -439,7 +447,6 @@ public class TimeLineController { private long getCaseLastArtifactID(final SleuthkitCase sleuthkitCase) { long caseLastArtfId = -1; String query = "select Max(artifact_id) as max_id from blackboard_artifacts"; // NON-NLS - try (CaseDbQuery dbQuery = sleuthkitCase.executeQuery(query)) { ResultSet resultSet = dbQuery.getResultSet(); while (resultSet.next()) { @@ -556,12 +563,12 @@ public class TimeLineController { @NbBundle.Messages({"# {0} - the number of events", "Timeline.pushDescrLOD.confdlg.msg=You are about to show details for {0} events." + " This might be very slow or even crash Autopsy.\n\nDo you want to continue?"}) - synchronized public boolean pushDescrLOD(DescriptionLOD newLOD) { + synchronized public boolean pushDescrLOD(DescriptionLoD newLOD) { Map eventCounts = filteredEvents.getEventCounts(filteredEvents.zoomParametersProperty().get().getTimeRange()); final Long count = eventCounts.values().stream().reduce(0l, Long::sum); boolean shouldContinue = true; - if (newLOD == DescriptionLOD.FULL && count > 10_000) { + if (newLOD == DescriptionLoD.FULL && count > 10_000) { String format = NumberFormat.getInstance().format(count); int showConfirmDialog = JOptionPane.showConfirmDialog(mainFrame, @@ -616,7 +623,6 @@ public class TimeLineController { synchronized private void advance(ZoomParams newState) { historyManager.advance(newState); - } public void selectTimeAndType(Interval interval, EventType type) { @@ -811,6 +817,21 @@ public class TimeLineController { @Override public void propertyChange(PropertyChangeEvent evt) { + /** + * Checking for a current case is a stop gap measure until a + * different way of handling the closing of cases is worked out. + * Currently, remote events may be received for a case that is + * already closed. + */ + try { + Case.getCurrentCase(); + } catch (IllegalStateException notUsed) { + /** + * Case is closed, do nothing. + */ + return; + } + switch (IngestManager.IngestModuleEvent.valueOf(evt.getPropertyName())) { case CONTENT_CHANGED: case DATA_ADDED: @@ -866,6 +887,7 @@ public class TimeLineController { case DATA_SOURCE_ADDED: SwingUtilities.invokeLater(TimeLineController.this::confirmOutOfDateRebuildIfWindowOpen); break; + case CURRENT_CASE: OpenTimelineAction.invalidateController(); SwingUtilities.invokeLater(TimeLineController.this::closeTimeLine); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java index 33e3f63ac1..f19dc9bf1c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java @@ -169,12 +169,12 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer new Forward(controller).handle(new ActionEvent()); } }); - controller.getViewMode().addListener((Observable observable) -> { - if (controller.getViewMode().get().equals(VisualizationMode.COUNTS)) { + controller.viewModeProperty().addListener((Observable observable) -> { + if (controller.viewModeProperty().get().equals(VisualizationMode.COUNTS)) { tabPane.getSelectionModel().select(filterTab); } }); - eventsTab.disableProperty().bind(controller.getViewMode().isEqualTo(VisualizationMode.COUNTS)); + eventsTab.disableProperty().bind(controller.viewModeProperty().isEqualTo(VisualizationMode.COUNTS)); visualizationPanel.setController(controller); navPanel.setController(controller); filtersPanel.setController(controller); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshot.java b/Core/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshot.java index 62305b4c79..bc11657244 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshot.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/actions/SaveSnapshot.java @@ -69,7 +69,7 @@ public class SaveSnapshot extends Action { //choose location/name DirectoryChooser fileChooser = new DirectoryChooser(); fileChooser.setTitle(NbBundle.getMessage(this.getClass(), "SaveSnapshot.fileChoose.title.text")); - fileChooser.setInitialDirectory(new File(Case.getCurrentCase().getCaseDirectory() + File.separator + "Reports")); // NON-NLS + fileChooser.setInitialDirectory(new File(Case.getCurrentCase().getReportDirectory())); File outFolder = fileChooser.showDialog(null); if (outFolder == null) { return; diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/AggregateEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/AggregateEvent.java deleted file mode 100644 index 6ca47c55be..0000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/AggregateEvent.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2013-15 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.timeline.datamodel; - -import com.google.common.collect.Sets; -import java.util.Collections; -import java.util.Set; -import javax.annotation.concurrent.Immutable; -import org.joda.time.Interval; -import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; -import org.sleuthkit.autopsy.timeline.utils.IntervalUtils; -import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; - -/** - * Represents a set of other (TimeLineEvent) events aggregated together. All the - * sub events should have the same type and matching descriptions at the - * designated 'zoom level'. - */ -@Immutable -public class AggregateEvent { - - /** - * the smallest time interval containing all the aggregated events - */ - final private Interval span; - - /** - * the type of all the aggregted events - */ - final private EventType type; - - /** - * the common description of all the aggregated events - */ - final private String description; - - /** - * the description level of detail that the events were aggregated at. - */ - private final DescriptionLOD lod; - - /** - * the set of ids of the aggregated events - */ - final private Set eventIDs; - - /** - * the ids of the subset of aggregated events that have at least one tag - * applied to them - */ - private final Set tagged; - - /** - * the ids of the subset of aggregated events that have at least one hash - * set hit - */ - private final Set hashHits; - - public AggregateEvent(Interval spanningInterval, EventType type, Set eventIDs, Set hashHits, Set tagged, String description, DescriptionLOD lod) { - - this.span = spanningInterval; - this.type = type; - this.hashHits = hashHits; - this.tagged = tagged; - this.description = description; - this.eventIDs = eventIDs; - this.lod = lod; - } - - /** - * @return the actual interval from the first event to the last event - */ - public Interval getSpan() { - return span; - } - - public Set getEventIDs() { - return Collections.unmodifiableSet(eventIDs); - } - - public Set getEventIDsWithHashHits() { - return Collections.unmodifiableSet(hashHits); - } - - public Set getEventIDsWithTags() { - return Collections.unmodifiableSet(tagged); - } - - public String getDescription() { - return description; - } - - public EventType getType() { - return type; - } - - public DescriptionLOD getLOD() { - return lod; - } - - /** - * merge two aggregate events into one new aggregate event. - * - * @param aggEvent1 - * @param aggEVent2 - * - * @return a new aggregate event that is the result of merging the given - * events - */ - public static AggregateEvent merge(AggregateEvent aggEvent1, AggregateEvent ag2) { - - if (aggEvent1.getType() != ag2.getType()) { - throw new IllegalArgumentException("aggregate events are not compatible they have different types"); - } - - if (!aggEvent1.getDescription().equals(ag2.getDescription())) { - throw new IllegalArgumentException("aggregate events are not compatible they have different descriptions"); - } - Sets.SetView idsUnion = Sets.union(aggEvent1.getEventIDs(), ag2.getEventIDs()); - Sets.SetView hashHitsUnion = Sets.union(aggEvent1.getEventIDsWithHashHits(), ag2.getEventIDsWithHashHits()); - Sets.SetView taggedUnion = Sets.union(aggEvent1.getEventIDsWithTags(), ag2.getEventIDsWithTags()); - - return new AggregateEvent(IntervalUtils.span(aggEvent1.span, ag2.span), aggEvent1.getType(), idsUnion, hashHitsUnion, taggedUnion, aggEvent1.getDescription(), aggEvent1.lod); - } -} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java new file mode 100644 index 0000000000..ba3333e7f7 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventBundle.java @@ -0,0 +1,55 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.timeline.datamodel; + +import java.util.Optional; +import java.util.Set; +import java.util.SortedSet; +import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; + +/** + * A interface for groups of events that share some attributes in common. + */ +public interface EventBundle> { + + String getDescription(); + + DescriptionLoD getDescriptionLoD(); + + Set getEventIDs(); + + Set getEventIDsWithHashHits(); + + Set getEventIDsWithTags(); + + EventType getEventType(); + + long getEndMillis(); + + long getStartMillis(); + + Optional getParentBundle(); + + default long getCount() { + return getEventIDs().size(); + } + + SortedSet getClusters(); +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java new file mode 100644 index 0000000000..592c4674f6 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventCluster.java @@ -0,0 +1,191 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-15 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.timeline.datamodel; + +import com.google.common.collect.ImmutableSortedSet; +import com.google.common.collect.Sets; +import java.util.Collections; +import java.util.Comparator; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.SortedSet; +import javax.annotation.concurrent.Immutable; +import org.joda.time.Interval; +import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; +import org.sleuthkit.autopsy.timeline.utils.IntervalUtils; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; + +/** + * Represents a set of other (TimeLineEvent) events clustered together. All the + * sub events should have the same type and matching descriptions at the + * designated 'zoom level', and be 'close together' in time. + */ +@Immutable +public class EventCluster implements EventBundle { + + /** + * merge two event clusters into one new event cluster. + * + * @param cluster1 + * @param cluster2 + * + * @return a new event cluster that is the result of merging the given + * events clusters + */ + public static EventCluster merge(EventCluster cluster1, EventCluster cluster2) { + if (cluster1.getEventType() != cluster2.getEventType()) { + throw new IllegalArgumentException("event clusters are not compatible: they have different types"); + } + + if (!cluster1.getDescription().equals(cluster2.getDescription())) { + throw new IllegalArgumentException("event clusters are not compatible: they have different descriptions"); + } + Sets.SetView idsUnion = Sets.union(cluster1.getEventIDs(), cluster2.getEventIDs()); + Sets.SetView hashHitsUnion = Sets.union(cluster1.getEventIDsWithHashHits(), cluster2.getEventIDsWithHashHits()); + Sets.SetView taggedUnion = Sets.union(cluster1.getEventIDsWithTags(), cluster2.getEventIDsWithTags()); + + return new EventCluster(IntervalUtils.span(cluster1.span, cluster2.span), cluster1.getEventType(), idsUnion, hashHitsUnion, taggedUnion, cluster1.getDescription(), cluster1.lod); + } + + final private EventStripe parent; + + /** + * the smallest time interval containing all the clustered events + */ + final private Interval span; + + /** + * the type of all the clustered events + */ + final private EventType type; + + /** + * the common description of all the clustered events + */ + final private String description; + + /** + * the description level of detail that the events were clustered at. + */ + private final DescriptionLoD lod; + + /** + * the set of ids of the clustered events + */ + final private Set eventIDs; + + /** + * the ids of the subset of clustered events that have at least one tag + * applied to them + */ + private final Set tagged; + + /** + * the ids of the subset of clustered events that have at least one hash set + * hit + */ + private final Set hashHits; + + private EventCluster(Interval spanningInterval, EventType type, Set eventIDs, Set hashHits, Set tagged, String description, DescriptionLoD lod, EventStripe parent) { + + this.span = spanningInterval; + this.type = type; + this.hashHits = hashHits; + this.tagged = tagged; + this.description = description; + this.eventIDs = eventIDs; + this.lod = lod; + this.parent = parent; + } + + public EventCluster(Interval spanningInterval, EventType type, Set eventIDs, Set hashHits, Set tagged, String description, DescriptionLoD lod) { + this(spanningInterval, type, eventIDs, hashHits, tagged, description, lod, null); + } + + @Override + public Optional getParentBundle() { + return Optional.ofNullable(parent); + } + + public Interval getSpan() { + return span; + } + + @Override + public long getStartMillis() { + return span.getStartMillis(); + } + + @Override + public long getEndMillis() { + return span.getEndMillis(); + } + + @Override + public Set getEventIDs() { + return Collections.unmodifiableSet(eventIDs); + } + + @Override + public Set getEventIDsWithHashHits() { + return Collections.unmodifiableSet(hashHits); + } + + @Override + public Set getEventIDsWithTags() { + return Collections.unmodifiableSet(tagged); + } + + @Override + public String getDescription() { + return description; + } + + @Override + public EventType getEventType() { + return type; + } + + @Override + public DescriptionLoD getDescriptionLoD() { + return lod; + } + + /** + * return a new EventCluster identical to this one, except with the given + * EventBundle as the parent. + * + * @param parent + * + * @return a new EventCluster identical to this one, except with the given + * EventBundle as the parent. + */ + public EventCluster withParent(EventStripe parent) { + if (Objects.nonNull(this.parent)) { + throw new IllegalStateException("Event Cluster already has a parent!"); + } + return new EventCluster(span, type, eventIDs, hashHits, tagged, description, lod, parent); + } + + @Override + public SortedSet< EventCluster> getClusters() { + return ImmutableSortedSet.orderedBy(Comparator.comparing(EventCluster::getStartMillis)).add(this).build(); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java new file mode 100644 index 0000000000..cd457d336e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/EventStripe.java @@ -0,0 +1,161 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.timeline.datamodel; + +import com.google.common.base.Preconditions; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import javax.annotation.concurrent.Immutable; +import org.python.google.common.base.Objects; +import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; + +/** + * A 'collection' of {@link EventCluster}s, all having the same type, + * description, and zoom levels, but not necessarily close together in time. + */ +@Immutable +public final class EventStripe implements EventBundle { + + public static EventStripe merge(EventStripe u, EventStripe v) { + Preconditions.checkNotNull(u); + Preconditions.checkNotNull(v); + Preconditions.checkArgument(Objects.equal(u.description, v.description)); + Preconditions.checkArgument(Objects.equal(u.lod, v.lod)); + Preconditions.checkArgument(Objects.equal(u.type, v.type)); + Preconditions.checkArgument(Objects.equal(u.parent, v.parent)); + return new EventStripe(u, v); + } + + private final EventCluster parent; + + private final SortedSet clusters = new TreeSet<>(Comparator.comparing(EventCluster::getStartMillis)); + + /** + * the type of all the events + */ + private final EventType type; + + /** + * the common description of all the events + */ + private final String description; + + /** + * the description level of detail that the events were clustered at. + */ + private final DescriptionLoD lod; + + /** + * the set of ids of the events + */ + private final Set eventIDs = new HashSet<>(); + + /** + * the ids of the subset of events that have at least one tag applied to + * them + */ + private final Set tagged = new HashSet<>(); + + /** + * the ids of the subset of events that have at least one hash set hit + */ + private final Set hashHits = new HashSet<>(); + + public EventStripe(EventCluster cluster, EventCluster parent) { + clusters.add(cluster); + + type = cluster.getEventType(); + description = cluster.getDescription(); + lod = cluster.getDescriptionLoD(); + eventIDs.addAll(cluster.getEventIDs()); + tagged.addAll(cluster.getEventIDsWithTags()); + hashHits.addAll(cluster.getEventIDsWithHashHits()); + this.parent = parent; + } + + private EventStripe(EventStripe u, EventStripe v) { + clusters.addAll(u.clusters); + clusters.addAll(v.clusters); + type = u.getEventType(); + description = u.getDescription(); + lod = u.getDescriptionLoD(); + eventIDs.addAll(u.getEventIDs()); + eventIDs.addAll(v.getEventIDs()); + tagged.addAll(u.getEventIDsWithTags()); + tagged.addAll(v.getEventIDsWithTags()); + hashHits.addAll(u.getEventIDsWithHashHits()); + hashHits.addAll(v.getEventIDsWithHashHits()); + parent = u.getParentBundle().orElse(v.getParentBundle().orElse(null)); + } + + @Override + public Optional getParentBundle() { + return Optional.ofNullable(parent); + } + + @Override + public String getDescription() { + return description; + } + + @Override + public EventType getEventType() { + return type; + } + + @Override + public DescriptionLoD getDescriptionLoD() { + return lod; + } + + @Override + public Set getEventIDs() { + return Collections.unmodifiableSet(eventIDs); + } + + @Override + public Set getEventIDsWithHashHits() { + return Collections.unmodifiableSet(hashHits); + } + + @Override + public Set getEventIDsWithTags() { + return Collections.unmodifiableSet(tagged); + } + + @Override + public long getStartMillis() { + return clusters.first().getStartMillis(); + } + + @Override + public long getEndMillis() { + return clusters.last().getEndMillis(); + } + + public SortedSet< EventCluster> getClusters() { + return Collections.unmodifiableSortedSet(clusters); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java index 3de6dacc61..ae354f87f8 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java @@ -20,6 +20,7 @@ package org.sleuthkit.autopsy.timeline.datamodel; import com.google.common.eventbus.EventBus; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -33,11 +34,13 @@ import javax.annotation.concurrent.GuardedBy; import org.joda.time.DateTimeZone; import org.joda.time.Interval; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.events.BlackBoardArtifactTagAddedEvent; +import org.sleuthkit.autopsy.casemodule.events.BlackBoardArtifactTagDeletedEvent; +import org.sleuthkit.autopsy.casemodule.events.BlackBoardArtifactTagDeletedEvent.DeletedBlackboardArtifactTagInfo; +import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; +import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; +import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent.DeletedContentTagInfo; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.events.BlackBoardArtifactTagAddedEvent; -import org.sleuthkit.autopsy.events.BlackBoardArtifactTagDeletedEvent; -import org.sleuthkit.autopsy.events.ContentTagAddedEvent; -import org.sleuthkit.autopsy.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.timeline.TimeLineView; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.RootEventType; @@ -55,7 +58,7 @@ import org.sleuthkit.autopsy.timeline.filters.TagNameFilter; import org.sleuthkit.autopsy.timeline.filters.TagsFilter; import org.sleuthkit.autopsy.timeline.filters.TextFilter; import org.sleuthkit.autopsy.timeline.filters.TypeFilter; -import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; import org.sleuthkit.datamodel.BlackboardArtifact; @@ -104,7 +107,7 @@ public final class FilteredEventsModel { private final ReadOnlyObjectWrapper< EventTypeZoomLevel> requestedTypeZoom = new ReadOnlyObjectWrapper<>(EventTypeZoomLevel.BASE_TYPE); @GuardedBy("this") - private final ReadOnlyObjectWrapper< DescriptionLOD> requestedLOD = new ReadOnlyObjectWrapper<>(DescriptionLOD.SHORT); + private final ReadOnlyObjectWrapper< DescriptionLoD> requestedLOD = new ReadOnlyObjectWrapper<>(DescriptionLoD.SHORT); @GuardedBy("this") private final ReadOnlyObjectWrapper requestedZoomParamters = new ReadOnlyObjectWrapper<>(); @@ -143,6 +146,7 @@ public final class FilteredEventsModel { }); requestedFilter.set(getDefaultFilter()); + //TODO: use bindings to keep these in sync? -jm requestedZoomParamters.addListener((Observable observable) -> { final ZoomParams zoomParams = requestedZoomParamters.get(); @@ -153,7 +157,7 @@ public final class FilteredEventsModel { || zoomParams.getTimeRange().equals(requestedTimeRange.get()) == false) { requestedTypeZoom.set(zoomParams.getTypeZoomLevel()); - requestedFilter.set(zoomParams.getFilter().copyOf()); + requestedFilter.set(zoomParams.getFilter()); requestedTimeRange.set(zoomParams.getTimeRange()); requestedLOD.set(zoomParams.getDescriptionLOD()); } @@ -178,7 +182,7 @@ public final class FilteredEventsModel { return requestedTimeRange.getReadOnlyProperty(); } - synchronized public ReadOnlyObjectProperty descriptionLODProperty() { + synchronized public ReadOnlyObjectProperty descriptionLODProperty() { return requestedLOD.getReadOnlyProperty(); } @@ -190,7 +194,7 @@ public final class FilteredEventsModel { return requestedTypeZoom.getReadOnlyProperty(); } - synchronized public DescriptionLOD getDescriptionLOD() { + synchronized public DescriptionLoD getDescriptionLOD() { return requestedLOD.get(); } @@ -227,7 +231,7 @@ public final class FilteredEventsModel { tagNameFilter.setSelected(Boolean.TRUE); tagsFilter.addSubFilter(tagNameFilter); }); - return new RootFilter(new HideKnownFilter(), tagsFilter, hashHitsFilter, new TextFilter(), new TypeFilter(RootEventType.getInstance()), dataSourcesFilter); + return new RootFilter(new HideKnownFilter(), tagsFilter, hashHitsFilter, new TextFilter(), new TypeFilter(RootEventType.getInstance()), dataSourcesFilter, Collections.emptySet()); } public Interval getBoundingEventsInterval() { @@ -320,24 +324,22 @@ public final class FilteredEventsModel { } /** - * @param aggregation * - * @return a list of aggregated events that are within the requested time - * range and pass the requested filter, using the given aggregation - * to control the grouping of events + * @return a list of event clusters at the requested zoom levels that are + * within the requested time range and pass the requested filter */ - public List getAggregatedEvents() { + public List getEventClusters() { final Interval range; final RootFilter filter; final EventTypeZoomLevel zoom; - final DescriptionLOD lod; + final DescriptionLoD lod; synchronized (this) { range = requestedTimeRange.get(); filter = requestedFilter.get(); zoom = requestedTypeZoom.get(); lod = requestedLOD.get(); } - return repo.getAggregatedEvents(new ZoomParams(range, zoom, filter, lod)); + return repo.getEventClusters(new ZoomParams(range, zoom, filter, lod)); } /** @@ -347,30 +349,30 @@ public final class FilteredEventsModel { * range and pass the requested filter, using the given aggregation * to control the grouping of events */ - public List getAggregatedEvents(ZoomParams params) { - return repo.getAggregatedEvents(params); + public List getEventClusters(ZoomParams params) { + return repo.getEventClusters(params); } synchronized public boolean handleContentTagAdded(ContentTagAddedEvent evt) { - ContentTag contentTag = evt.getTag(); + ContentTag contentTag = evt.getAddedTag(); Content content = contentTag.getContent(); Set updatedEventIDs = repo.addTag(content.getId(), null, contentTag); return postTagsUpdated(updatedEventIDs); } synchronized public boolean handleArtifactTagAdded(BlackBoardArtifactTagAddedEvent evt) { - BlackboardArtifactTag artifactTag = evt.getTag(); + BlackboardArtifactTag artifactTag = evt.getAddedTag(); BlackboardArtifact artifact = artifactTag.getArtifact(); Set updatedEventIDs = repo.addTag(artifact.getObjectID(), artifact.getArtifactID(), artifactTag);; return postTagsUpdated(updatedEventIDs); } synchronized public boolean handleContentTagDeleted(ContentTagDeletedEvent evt) { - ContentTag contentTag = evt.getTag(); - Content content = contentTag.getContent(); + DeletedContentTagInfo deletedTagInfo = evt.getDeletedTagInfo(); try { + Content content = autoCase.getSleuthkitCase().getContentById(deletedTagInfo.getContentID()); boolean tagged = autoCase.getServices().getTagsManager().getContentTagsByContent(content).isEmpty() == false; - Set updatedEventIDs = repo.deleteTag(content.getId(), null, contentTag, tagged); + Set updatedEventIDs = repo.deleteTag(content.getId(), null, deletedTagInfo.getTagID(), tagged); return postTagsUpdated(updatedEventIDs); } catch (TskCoreException ex) { LOGGER.log(Level.SEVERE, "unable to determine tagged status of content.", ex); @@ -379,11 +381,11 @@ public final class FilteredEventsModel { } synchronized public boolean handleArtifactTagDeleted(BlackBoardArtifactTagDeletedEvent evt) { - BlackboardArtifactTag artifactTag = evt.getTag(); - BlackboardArtifact artifact = artifactTag.getArtifact(); + DeletedBlackboardArtifactTagInfo deletedTagInfo = evt.getDeletedTagInfo(); try { + BlackboardArtifact artifact = autoCase.getSleuthkitCase().getBlackboardArtifact(deletedTagInfo.getArtifactID()); boolean tagged = autoCase.getServices().getTagsManager().getBlackboardArtifactTagsByArtifact(artifact).isEmpty() == false; - Set updatedEventIDs = repo.deleteTag(artifact.getObjectID(), artifact.getArtifactID(), artifactTag, tagged); + Set updatedEventIDs = repo.deleteTag(artifact.getObjectID(), artifact.getArtifactID(), deletedTagInfo.getTagID(), tagged); return postTagsUpdated(updatedEventIDs); } catch (TskCoreException ex) { LOGGER.log(Level.SEVERE, "unable to determine tagged status of artifact.", ex); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/TimeLineEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/TimeLineEvent.java index 744311c321..2296edfbf8 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/TimeLineEvent.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/TimeLineEvent.java @@ -22,7 +22,7 @@ import com.google.common.collect.ImmutableMap; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; -import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.datamodel.TskData; /** @@ -38,7 +38,7 @@ public class TimeLineEvent { private final long time; private final EventType subType; - private final ImmutableMap descriptions; + private final ImmutableMap descriptions; private final TskData.FileKnown known; private final boolean hashHit; @@ -50,10 +50,9 @@ public class TimeLineEvent { this.artifactID = artifactID == 0 ? null : artifactID; this.time = time; this.subType = type; - descriptions = ImmutableMap.of( - DescriptionLOD.FULL, fullDescription, - DescriptionLOD.MEDIUM, medDescription, - DescriptionLOD.SHORT, shortDescription); + descriptions = ImmutableMap.of(DescriptionLoD.FULL, fullDescription, + DescriptionLoD.MEDIUM, medDescription, + DescriptionLoD.SHORT, shortDescription); this.known = known; this.hashHit = hashHit; @@ -94,22 +93,22 @@ public class TimeLineEvent { } public String getFullDescription() { - return getDescription(DescriptionLOD.FULL); + return getDescription(DescriptionLoD.FULL); } public String getMedDescription() { - return getDescription(DescriptionLOD.MEDIUM); + return getDescription(DescriptionLoD.MEDIUM); } public String getShortDescription() { - return getDescription(DescriptionLOD.SHORT); + return getDescription(DescriptionLoD.SHORT); } public TskData.FileKnown getKnown() { return known; } - public String getDescription(DescriptionLOD lod) { + public String getDescription(DescriptionLoD lod) { return descriptions.get(lod); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/eventtype/MiscTypes.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/eventtype/MiscTypes.java index ef57868a1d..6f59c8f981 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/eventtype/MiscTypes.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/eventtype/MiscTypes.java @@ -23,10 +23,11 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.function.BiFunction; +import java.util.logging.Level; import javafx.scene.image.Image; import org.apache.commons.lang3.StringUtils; -import org.openide.util.Exceptions; import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; @@ -57,7 +58,7 @@ public enum MiscTypes implements EventType, ArtifactEventType { BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME, new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME), new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_LOCATION), - (BlackboardArtifact artf, Map attrMap) -> { + (artf, attrMap) -> { final BlackboardAttribute latStart = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_START); final BlackboardAttribute longStart = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_START); final BlackboardAttribute latEnd = attrMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_END); @@ -129,18 +130,16 @@ public enum MiscTypes implements EventType, ArtifactEventType { BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED, new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_MAKE), new AttributeExtractor(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_MODEL), - (BlackboardArtifact t, - Map u) -> { + (artifact, attributeMap) -> { try { - AbstractFile f = t.getSleuthkitCase().getAbstractFileById(t.getObjectID()); - if (f != null) { - return f.getName(); + AbstractFile file = artifact.getSleuthkitCase().getAbstractFileById(artifact.getObjectID()); + if (file != null) { + return file.getName(); } - return " error loading file name"; // NON-NLS } catch (TskCoreException ex) { - Exceptions.printStackTrace(ex); - return " error loading file name"; // NON-NLS + Logger.getLogger(MiscTypes.class.getName()).log(Level.SEVERE, "Exif event type failed to look up backing file name", ex); } + return " error loading file name"; // NON-NLS }), DEVICES_ATTACHED(NbBundle.getMessage(MiscTypes.class, "MiscTypes.devicesAttached.name"), "usb_devices.png", // NON-NLS BlackboardArtifact.ARTIFACT_TYPE.TSK_DEVICE_ATTACHED, @@ -185,26 +184,41 @@ public enum MiscTypes implements EventType, ArtifactEventType { private final BiFunction, String> shortExtractor; + /** + * {@inheritDoc } + */ @Override public BiFunction, String> getFullExtractor() { return longExtractor; } + /** + * {@inheritDoc } + */ @Override public BiFunction, String> getMedExtractor() { return medExtractor; } + /** + * {@inheritDoc } + */ @Override public BiFunction, String> getShortExtractor() { return shortExtractor; } + /** + * {@inheritDoc } + */ @Override public BlackboardAttribute.ATTRIBUTE_TYPE getDateTimeAttrubuteType() { return dateTimeAttributeType; } + /** + * {@inheritDoc } + */ @Override public EventTypeZoomLevel getZoomLevel() { return EventTypeZoomLevel.SUB_TYPE; diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java b/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java index 1b6c5943fb..37ba62eedb 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java @@ -54,7 +54,7 @@ import org.joda.time.Period; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.timeline.TimeLineController; -import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.BaseTypes; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; @@ -64,7 +64,7 @@ import static org.sleuthkit.autopsy.timeline.db.SQLHelper.useTagTablesHelper; import org.sleuthkit.autopsy.timeline.filters.RootFilter; import org.sleuthkit.autopsy.timeline.filters.TagsFilter; import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo; -import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; import org.sleuthkit.datamodel.SleuthkitCase; @@ -103,7 +103,7 @@ public class EventDB { } } - private static final java.util.logging.Logger LOGGER = Logger.getLogger(EventDB.class.getName()); + private static final org.sleuthkit.autopsy.coreutils.Logger LOGGER = Logger.getLogger(EventDB.class.getName()); static { //make sure sqlite driver is loaded, possibly redundant @@ -158,7 +158,8 @@ public class EventDB { private PreparedStatement dropHashSetsTableStmt; private PreparedStatement dropTagsTableStmt; private PreparedStatement dropDBInfoTableStmt; - private PreparedStatement selectEventIDsFromOBjectAndArtifactStmt; + private PreparedStatement selectNonArtifactEventIDsByObjectIDStmt; + private PreparedStatement selectEventIDsBYObjectAndArtifactIDStmt; private final Set preparedStatements = new HashSet<>(); @@ -413,10 +414,7 @@ public class EventDB { try (ResultSet rs = getDataSourceIDsStmt.executeQuery()) { while (rs.next()) { long datasourceID = rs.getLong("datasource_id"); - //this relies on the fact that no tskObj has ID 0 but 0 is the default value for the datasource_id column in the events table. - if (datasourceID != 0) { - hashSet.add(datasourceID); - } + hashSet.add(datasourceID); } } catch (SQLException ex) { LOGGER.log(Level.SEVERE, "Failed to get MAX time.", ex); // NON-NLS @@ -583,6 +581,10 @@ public class EventDB { initializeTagsTable(); + createIndex("events", Arrays.asList("datasource_id")); + createIndex("events", Arrays.asList("event_id", "hash_hit")); + createIndex("events", Arrays.asList("event_id", "tagged")); + createIndex("events", Arrays.asList("file_id")); createIndex("events", Arrays.asList("file_id")); createIndex("events", Arrays.asList("artifact_id")); createIndex("events", Arrays.asList("time")); @@ -595,7 +597,7 @@ public class EventDB { "INSERT INTO events (datasource_id,file_id ,artifact_id, time, sub_type, base_type, full_description, med_description, short_description, known_state, hash_hit, tagged) " // NON-NLS + "VALUES (?,?,?,?,?,?,?,?,?,?,?,?)"); // NON-NLS getHashSetNamesStmt = prepareStatement("SELECT hash_set_id, hash_set_name FROM hash_sets"); // NON-NLS - getDataSourceIDsStmt = prepareStatement("SELECT DISTINCT datasource_id FROM events"); // NON-NLS + getDataSourceIDsStmt = prepareStatement("SELECT DISTINCT datasource_id FROM events WHERE datasource_id != 0"); // NON-NLS getMaxTimeStmt = prepareStatement("SELECT Max(time) AS max FROM events"); // NON-NLS getMinTimeStmt = prepareStatement("SELECT Min(time) AS min FROM events"); // NON-NLS getEventByIDStmt = prepareStatement("SELECT * FROM events WHERE event_id = ?"); // NON-NLS @@ -612,7 +614,8 @@ public class EventDB { dropHashSetsTableStmt = prepareStatement("DROP TABLE IF EXISTS hash_sets"); dropTagsTableStmt = prepareStatement("DROP TABLE IF EXISTS tags"); dropDBInfoTableStmt = prepareStatement("DROP TABLE IF EXISTS db_ino"); - selectEventIDsFromOBjectAndArtifactStmt = prepareStatement("SELECT event_id FROM events WHERE file_id == ? AND artifact_id IS ?"); + selectNonArtifactEventIDsByObjectIDStmt = prepareStatement("SELECT event_id FROM events WHERE file_id == ? AND artifact_id IS NULL"); + selectEventIDsBYObjectAndArtifactIDStmt = prepareStatement("SELECT event_id FROM events WHERE file_id == ? AND artifact_id = ?"); } catch (SQLException sQLException) { LOGGER.log(Level.SEVERE, "failed to prepareStatment", sQLException); // NON-NLS } @@ -850,16 +853,15 @@ public class EventDB { * * @return the event ids that match the object/artifact pair */ - Set deleteTag(long objectID, @Nullable Long artifactID, Tag tag, boolean stillTagged) { + Set deleteTag(long objectID, @Nullable Long artifactID, long tagID, boolean stillTagged) { DBLock.lock(); try { //"DELETE FROM tags WHERE tag_id = ? deleteTagStmt.clearParameters(); - deleteTagStmt.setLong(1, tag.getId()); + deleteTagStmt.setLong(1, tagID); deleteTagStmt.executeUpdate(); - Set eventIDs = markEventsTagged(objectID, artifactID, stillTagged); - return eventIDs; + return markEventsTagged(objectID, artifactID, stillTagged); } catch (SQLException ex) { LOGGER.log(Level.SEVERE, "failed to add tag to event", ex); // NON-NLS } finally { @@ -889,23 +891,29 @@ public class EventDB { * (un)taggedS */ private Set markEventsTagged(long objectID, @Nullable Long artifactID, boolean tagged) throws SQLException { - //first select the matching event ids - selectEventIDsFromOBjectAndArtifactStmt.clearParameters(); - selectEventIDsFromOBjectAndArtifactStmt.setLong(1, objectID); + + PreparedStatement selectStmt; if (Objects.isNull(artifactID)) { - selectEventIDsFromOBjectAndArtifactStmt.setNull(2, Types.NULL); + //"SELECT event_id FROM events WHERE file_id == ? AND artifact_id IS NULL" + selectNonArtifactEventIDsByObjectIDStmt.clearParameters(); + selectNonArtifactEventIDsByObjectIDStmt.setLong(1, objectID); + selectStmt = selectNonArtifactEventIDsByObjectIDStmt; } else { - selectEventIDsFromOBjectAndArtifactStmt.setLong(2, artifactID); + //"SELECT event_id FROM events WHERE file_id == ? AND artifact_id = ?" + selectEventIDsBYObjectAndArtifactIDStmt.clearParameters(); + selectEventIDsBYObjectAndArtifactIDStmt.setLong(1, objectID); + selectEventIDsBYObjectAndArtifactIDStmt.setLong(2, artifactID); + selectStmt = selectEventIDsBYObjectAndArtifactIDStmt; } HashSet eventIDs = new HashSet<>(); - try (ResultSet executeQuery = selectEventIDsFromOBjectAndArtifactStmt.executeQuery();) { + try (ResultSet executeQuery = selectStmt.executeQuery();) { while (executeQuery.next()) { eventIDs.add(executeQuery.getLong("event_id")); } } - //then update tagged state for all event with selected ids + //update tagged state for all event with selected ids try (Statement updateStatement = con.createStatement();) { updateStatement.executeUpdate("UPDATE events SET tagged = " + (tagged ? 1 : 0) + " WHERE event_id IN (" + StringUtils.join(eventIDs, ",") + ")"); @@ -1031,7 +1039,7 @@ public class EventDB { } /** - * get a list of {@link AggregateEvent}s, clustered according to the given + * get a list of {@link EventCluster}s, clustered according to the given * zoom paramaters. * * @param params the zoom params that determine the zooming, filtering and @@ -1041,11 +1049,11 @@ public class EventDB { * the supplied filter, aggregated according to the given event type * and description zoom levels */ - List getAggregatedEvents(ZoomParams params) { + List getClusteredEvents(ZoomParams params) { //unpack params Interval timeRange = params.getTimeRange(); RootFilter filter = params.getFilter(); - DescriptionLOD descriptionLOD = params.getDescriptionLOD(); + DescriptionLoD descriptionLOD = params.getDescriptionLOD(); EventTypeZoomLevel typeZoomLevel = params.getTypeZoomLevel(); //ensure length of querried interval is not 0 @@ -1064,22 +1072,27 @@ public class EventDB { String timeZone = TimeLineController.getTimeZone().get().equals(TimeZone.getDefault()) ? ", 'localtime'" : ""; // NON-NLS String typeColumn = typeColumnHelper(useSubTypes); - //compose query string + //compose query string, new-lines only for nicer formatting if printing the entire query String query = "SELECT strftime('" + strfTimeFormat + "',time , 'unixepoch'" + timeZone + ") AS interval," // NON-NLS - + " group_concat(events.event_id) as event_ids, min(time), max(time), " + typeColumn + ", " + descriptionColumn // NON-NLS + + "\n group_concat(events.event_id) as event_ids," + + "\n group_concat(CASE WHEN hash_hit = 1 THEN events.event_id ELSE NULL END) as hash_hits," + + "\n group_concat(CASE WHEN tagged = 1 THEN events.event_id ELSE NULL END) as taggeds," + + "\n min(time), max(time), " + typeColumn + ", " + descriptionColumn // NON-NLS + "\n FROM events" + useHashHitTablesHelper(filter) + useTagTablesHelper(filter) // NON-NLS + "\n WHERE time >= " + start + " AND time < " + end + " AND " + SQLHelper.getSQLWhere(filter) // NON-NLS + "\n GROUP BY interval, " + typeColumn + " , " + descriptionColumn // NON-NLS + "\n ORDER BY min(time)"; // NON-NLS + System.out.println(query); // perform query and map results to AggregateEvent objects - List events = new ArrayList<>(); + List events = new ArrayList<>(); + DBLock.lock(); try (Statement createStatement = con.createStatement(); ResultSet rs = createStatement.executeQuery(query)) { while (rs.next()) { - events.add(aggregateEventHelper(rs, useSubTypes, descriptionLOD, filter.getTagsFilter())); + events.add(eventClusterHelper(rs, useSubTypes, descriptionLOD, filter.getTagsFilter())); } } catch (SQLException ex) { LOGGER.log(Level.SEVERE, "Failed to get aggregate events with query: " + query, ex); // NON-NLS @@ -1087,11 +1100,11 @@ public class EventDB { DBLock.unlock(); } - return mergeAggregateEvents(rangeInfo.getPeriodSize().getPeriod(), events); + return mergeEventClusters(rangeInfo.getPeriodSize().getPeriod(), events); } /** - * map a single row in a ResultSet to an AggregateEvent + * map a single row in a ResultSet to an EventCluster * * @param rs the result set whose current row should be mapped * @param useSubTypes use the sub_type column if true, else use the @@ -1103,32 +1116,17 @@ public class EventDB { * * @throws SQLException */ - private AggregateEvent aggregateEventHelper(ResultSet rs, boolean useSubTypes, DescriptionLOD descriptionLOD, TagsFilter filter) throws SQLException { + private EventCluster eventClusterHelper(ResultSet rs, boolean useSubTypes, DescriptionLoD descriptionLOD, TagsFilter filter) throws SQLException { Interval interval = new Interval(rs.getLong("min(time)") * 1000, rs.getLong("max(time)") * 1000, TimeLineController.getJodaTimeZone());// NON-NLS String eventIDsString = rs.getString("event_ids");// NON-NLS Set eventIDs = SQLHelper.unGroupConcat(eventIDsString, Long::valueOf); String description = rs.getString(SQLHelper.getDescriptionColumn(descriptionLOD)); EventType type = useSubTypes ? RootEventType.allTypes.get(rs.getInt("sub_type")) : BaseTypes.values()[rs.getInt("base_type")];// NON-NLS - Set hashHits = new HashSet<>(); - String hashHitQuery = "SELECT group_concat(event_id) FROM events WHERE event_id IN (" + eventIDsString + ") AND hash_hit = 1";// NON-NLS - try (Statement stmt = con.createStatement(); - ResultSet hashHitsRS = stmt.executeQuery(hashHitQuery)) { - while (hashHitsRS.next()) { - hashHits = SQLHelper.unGroupConcat(hashHitsRS.getString("group_concat(event_id)"), Long::valueOf);// NON-NLS - } - } + Set hashHits = SQLHelper.unGroupConcat(rs.getString("hash_hits"), Long::valueOf); + Set tagged = SQLHelper.unGroupConcat(rs.getString("taggeds"), Long::valueOf); - Set tagged = new HashSet<>(); - String taggedQuery = "SELECT group_concat(event_id) FROM events WHERE event_id IN (" + eventIDsString + ") AND tagged = 1";// NON-NLS - try (Statement stmt = con.createStatement(); - ResultSet taggedRS = stmt.executeQuery(taggedQuery)) { - while (taggedRS.next()) { - tagged = SQLHelper.unGroupConcat(taggedRS.getString("group_concat(event_id)"), Long::valueOf);// NON-NLS - } - } - - return new AggregateEvent(interval, type, eventIDs, hashHits, tagged, + return new EventCluster(interval, type, eventIDs, hashHits, tagged, description, descriptionLOD); } @@ -1145,36 +1143,36 @@ public class EventDB { * * @return */ - static private List mergeAggregateEvents(Period timeUnitLength, List preMergedEvents) { + static private List mergeEventClusters(Period timeUnitLength, List preMergedEvents) { //effectively map from type to (map from description to events) - Map> typeMap = new HashMap<>(); + Map> typeMap = new HashMap<>(); - for (AggregateEvent aggregateEvent : preMergedEvents) { - typeMap.computeIfAbsent(aggregateEvent.getType(), eventType -> HashMultimap.create()) + for (EventCluster aggregateEvent : preMergedEvents) { + typeMap.computeIfAbsent(aggregateEvent.getEventType(), eventType -> HashMultimap.create()) .put(aggregateEvent.getDescription(), aggregateEvent); } //result list to return - ArrayList aggEvents = new ArrayList<>(); + ArrayList aggEvents = new ArrayList<>(); //For each (type, description) key, merge agg events - for (SetMultimap descrMap : typeMap.values()) { + for (SetMultimap descrMap : typeMap.values()) { //for each description ... for (String descr : descrMap.keySet()) { //run through the sorted events, merging together adjacent events - Iterator iterator = descrMap.get(descr).stream() + Iterator iterator = descrMap.get(descr).stream() .sorted(Comparator.comparing(event -> event.getSpan().getStartMillis())) .iterator(); - AggregateEvent current = iterator.next(); + EventCluster current = iterator.next(); while (iterator.hasNext()) { - AggregateEvent next = iterator.next(); + EventCluster next = iterator.next(); Interval gap = current.getSpan().gap(next.getSpan()); //if they overlap or gap is less one quarter timeUnitLength //TODO: 1/4 factor is arbitrary. review! -jm if (gap == null || gap.toDuration().getMillis() <= timeUnitLength.toDurationFrom(gap.getStart()).getMillis() / 4) { //merge them - current = AggregateEvent.merge(current, next); + current = EventCluster.merge(current, next); } else { //done merging into current, set next as new current aggEvents.add(current); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java index 7a537626fd..73800b9b89 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java @@ -45,7 +45,7 @@ import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.services.TagsManager; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.timeline.ProgressWindow; -import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.ArtifactEventType; @@ -98,7 +98,7 @@ public class EventsRepository { private final LoadingCache idToEventCache; private final LoadingCache> eventCountsCache; - private final LoadingCache> aggregateEventsCache; + private final LoadingCache> eventClusterCache; private final ObservableMap datasourcesMap = FXCollections.observableHashMap(); private final ObservableMap hashSetMap = FXCollections.observableHashMap(); @@ -146,10 +146,10 @@ public class EventsRepository { .maximumSize(1000L) .expireAfterAccess(10, TimeUnit.MINUTES) .build(CacheLoader.from(eventDB::countEventsByType)); - aggregateEventsCache = CacheBuilder.newBuilder() + eventClusterCache = CacheBuilder.newBuilder() .maximumSize(1000L) .expireAfterAccess(10, TimeUnit.MINUTES - ).build(CacheLoader.from(eventDB::getAggregatedEvents)); + ).build(CacheLoader.from(eventDB::getClusteredEvents)); maxCache = CacheBuilder.newBuilder().build(CacheLoader.from(eventDB::getMaxTime)); minCache = CacheBuilder.newBuilder().build(CacheLoader.from(eventDB::getMinTime)); this.modelInstance = new FilteredEventsModel(this, currentStateProperty); @@ -206,8 +206,8 @@ public class EventsRepository { } - synchronized public List getAggregatedEvents(ZoomParams params) { - return aggregateEventsCache.getUnchecked(params); + synchronized public List getEventClusters(ZoomParams params) { + return eventClusterCache.getUnchecked(params); } synchronized public Map countEvents(ZoomParams params) { @@ -218,7 +218,7 @@ public class EventsRepository { minCache.invalidateAll(); maxCache.invalidateAll(); eventCountsCache.invalidateAll(); - aggregateEventsCache.invalidateAll(); + eventClusterCache.invalidateAll(); idToEventCache.invalidateAll(); } @@ -282,8 +282,8 @@ public class EventsRepository { return updatedEventIDs; } - synchronized public Set deleteTag(long objID, Long artifactID, Tag tag, boolean tagged) { - Set updatedEventIDs = eventDB.deleteTag(objID, artifactID, tag, tagged); + synchronized public Set deleteTag(long objID, Long artifactID, long tagID, boolean tagged) { + Set updatedEventIDs = eventDB.deleteTag(objID, artifactID, tagID, tagged); if (!updatedEventIDs.isEmpty()) { invalidateCaches(updatedEventIDs); } @@ -292,7 +292,7 @@ public class EventsRepository { synchronized private void invalidateCaches(Set updatedEventIDs) { eventCountsCache.invalidateAll(); - aggregateEventsCache.invalidateAll(); + eventClusterCache.invalidateAll(); idToEventCache.invalidateAll(updatedEventIDs); try { tagNames.setAll(autoCase.getSleuthkitCase().getTagNamesInUse()); @@ -487,26 +487,29 @@ public class EventsRepository { final String uniquePath = f.getUniquePath(); final String parentPath = f.getParentPath(); long datasourceID = f.getDataSource().getId(); - String datasourceName = StringUtils.substringBefore(StringUtils.stripStart(uniquePath, "/"), parentPath); - String rootFolder = StringUtils.substringBetween(parentPath, "/", "/"); - String shortDesc = datasourceName + "/" + StringUtils.defaultIfBlank(rootFolder, ""); - String medD = datasourceName + parentPath; + String datasourceName = StringUtils.substringBeforeLast(uniquePath, parentPath); + + String rootFolder = StringUtils.substringBefore(StringUtils.substringAfter(parentPath, "/"), "/"); + String shortDesc = datasourceName + "/" + StringUtils.defaultString(rootFolder); + shortDesc = shortDesc.endsWith("/") ? shortDesc : shortDesc + "/"; + String medDesc = datasourceName + parentPath; + final TskData.FileKnown known = f.getKnown(); Set hashSets = f.getHashSetNames(); List tags = tagsManager.getContentTagsByContent(f); //insert it into the db if time is > 0 => time is legitimate (drops logical files) if (f.getAtime() > 0) { - eventDB.insertEvent(f.getAtime(), FileSystemTypes.FILE_ACCESSED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, tags, trans); + eventDB.insertEvent(f.getAtime(), FileSystemTypes.FILE_ACCESSED, datasourceID, fID, null, uniquePath, medDesc, shortDesc, known, hashSets, tags, trans); } if (f.getMtime() > 0) { - eventDB.insertEvent(f.getMtime(), FileSystemTypes.FILE_MODIFIED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, tags, trans); + eventDB.insertEvent(f.getMtime(), FileSystemTypes.FILE_MODIFIED, datasourceID, fID, null, uniquePath, medDesc, shortDesc, known, hashSets, tags, trans); } if (f.getCtime() > 0) { - eventDB.insertEvent(f.getCtime(), FileSystemTypes.FILE_CHANGED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, tags, trans); + eventDB.insertEvent(f.getCtime(), FileSystemTypes.FILE_CHANGED, datasourceID, fID, null, uniquePath, medDesc, shortDesc, known, hashSets, tags, trans); } if (f.getCrtime() > 0) { - eventDB.insertEvent(f.getCrtime(), FileSystemTypes.FILE_CREATED, datasourceID, fID, null, uniquePath, medD, shortDesc, known, hashSets, tags, trans); + eventDB.insertEvent(f.getCrtime(), FileSystemTypes.FILE_CREATED, datasourceID, fID, null, uniquePath, medDesc, shortDesc, known, hashSets, tags, trans); } publish(new ProgressWindow.ProgressUpdate(i, numFiles, @@ -619,4 +622,9 @@ public class EventsRepository { } } } + + public boolean areFiltersEquivalent(RootFilter f1, RootFilter f2) { + return SQLHelper.getSQLWhere(f1).equals(SQLHelper.getSQLWhere(f2)); + + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/SQLHelper.java b/Core/src/org/sleuthkit/autopsy/timeline/db/SQLHelper.java index d807bcc741..65405d7930 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/SQLHelper.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/SQLHelper.java @@ -30,6 +30,7 @@ import org.sleuthkit.autopsy.timeline.datamodel.eventtype.RootEventType; import org.sleuthkit.autopsy.timeline.filters.AbstractFilter; import org.sleuthkit.autopsy.timeline.filters.DataSourceFilter; import org.sleuthkit.autopsy.timeline.filters.DataSourcesFilter; +import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter; import org.sleuthkit.autopsy.timeline.filters.Filter; import org.sleuthkit.autopsy.timeline.filters.HashHitsFilter; import org.sleuthkit.autopsy.timeline.filters.HashSetFilter; @@ -42,9 +43,9 @@ import org.sleuthkit.autopsy.timeline.filters.TextFilter; import org.sleuthkit.autopsy.timeline.filters.TypeFilter; import org.sleuthkit.autopsy.timeline.filters.UnionFilter; import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo; -import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; -import static org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD.FULL; -import static org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD.MEDIUM; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; +import static org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD.FULL; +import static org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD.MEDIUM; import static org.sleuthkit.autopsy.timeline.zooming.TimeUnits.DAYS; import static org.sleuthkit.autopsy.timeline.zooming.TimeUnits.HOURS; import static org.sleuthkit.autopsy.timeline.zooming.TimeUnits.MINUTES; @@ -57,16 +58,16 @@ import org.sleuthkit.datamodel.TskData; * Static helper methods for converting between java data model objects and * sqlite queries. */ -public class SQLHelper { +class SQLHelper { static String useHashHitTablesHelper(RootFilter filter) { HashHitsFilter hashHitFilter = filter.getHashHitsFilter(); - return hashHitFilter.isSelected() && false == hashHitFilter.isDisabled() ? " LEFT JOIN hash_set_hits " : " "; + return hashHitFilter.isActive() ? " LEFT JOIN hash_set_hits " : " "; } static String useTagTablesHelper(RootFilter filter) { TagsFilter tagsFilter = filter.getTagsFilter(); - return tagsFilter.isSelected() && false == tagsFilter.isDisabled() ? " LEFT JOIN tags " : " "; + return tagsFilter.isActive() ? " LEFT JOIN tags " : " "; } /** @@ -105,10 +106,20 @@ public class SQLHelper { return getSQLWhere((IntersectionFilter) filter); } + /** + * NOTE: I don't like this if-else instance of chain, but I can't decide + * what to do instead -jm + * + * @param filter + * + * @return + */ private static String getSQLWhere(Filter filter) { String result = ""; if (filter == null) { return "1"; + } else if (filter instanceof DescriptionFilter) { + result = getSQLWhere((DescriptionFilter) filter); } else if (filter instanceof TagsFilter) { result = getSQLWhere((TagsFilter) filter); } else if (filter instanceof HashHitsFilter) { @@ -130,7 +141,7 @@ public class SQLHelper { } else if (filter instanceof UnionFilter) { result = getSQLWhere((UnionFilter) filter); } else { - return "1"; + throw new IllegalArgumentException("getSQLWhere not defined for " + filter.getClass().getCanonicalName()); } result = StringUtils.deleteWhitespace(result).equals("(1and1and1)") ? "1" : result; result = StringUtils.deleteWhitespace(result).equals("()") ? "1" : result; @@ -138,16 +149,24 @@ public class SQLHelper { } private static String getSQLWhere(HideKnownFilter filter) { - if (filter.isSelected()) { + if (filter.isActive()) { return "(known_state IS NOT '" + TskData.FileKnown.KNOWN.getFileKnownValue() + "')"; // NON-NLS } else { return "1"; } } + private static String getSQLWhere(DescriptionFilter filter) { + if (filter.isActive()) { + String likeOrNotLike = (filter.getFilterMode() == DescriptionFilter.FilterMode.INCLUDE ? "" : " NOT") + " LIKE '"; + return "(" + getDescriptionColumn(filter.getDescriptionLoD()) + likeOrNotLike + filter.getDescription() + "' )"; // NON-NLS + } else { + return "1"; + } + } + private static String getSQLWhere(TagsFilter filter) { - if (filter.isSelected() - && (false == filter.isDisabled()) + if (filter.isActive() && (filter.getSubFilters().isEmpty() == false)) { String tagNameIDs = filter.getSubFilters().stream() .filter((TagNameFilter t) -> t.isSelected() && !t.isDisabled()) @@ -162,8 +181,7 @@ public class SQLHelper { } private static String getSQLWhere(HashHitsFilter filter) { - if (filter.isSelected() - && (false == filter.isDisabled()) + if (filter.isActive() && (filter.getSubFilters().isEmpty() == false)) { String hashSetIDs = filter.getSubFilters().stream() .filter((HashSetFilter t) -> t.isSelected() && !t.isDisabled()) @@ -176,7 +194,7 @@ public class SQLHelper { } private static String getSQLWhere(DataSourceFilter filter) { - if (filter.isSelected()) { + if (filter.isActive()) { return "(datasource_id = '" + filter.getDataSourceID() + "')"; } else { return "1"; @@ -184,15 +202,15 @@ public class SQLHelper { } private static String getSQLWhere(DataSourcesFilter filter) { - return (filter.isSelected()) ? "(datasource_id in (" + return (filter.isActive()) ? "(datasource_id in (" + filter.getSubFilters().stream() - .filter(AbstractFilter::isSelected) + .filter(AbstractFilter::isActive) .map((dataSourceFilter) -> String.valueOf(dataSourceFilter.getDataSourceID())) .collect(Collectors.joining(", ")) + "))" : "1"; } private static String getSQLWhere(TextFilter filter) { - if (filter.isSelected()) { + if (filter.isActive()) { if (StringUtils.isBlank(filter.getText())) { return "1"; } @@ -218,7 +236,7 @@ public class SQLHelper { return "0"; } else if (typeFilter.getEventType() instanceof RootEventType) { if (typeFilter.getSubFilters().stream() - .allMatch(subFilter -> subFilter.isSelected() && subFilter.getSubFilters().stream().allMatch(Filter::isSelected))) { + .allMatch(subFilter -> subFilter.isActive() && subFilter.getSubFilters().stream().allMatch(Filter::isActive))) { return "1"; //then collapse clause to true } } @@ -226,7 +244,7 @@ public class SQLHelper { } private static List getActiveSubTypes(TypeFilter filter) { - if (filter.isSelected()) { + if (filter.isActive()) { if (filter.getSubFilters().isEmpty()) { return Collections.singletonList(RootEventType.allTypes.indexOf(filter.getEventType())); } else { @@ -266,7 +284,7 @@ public class SQLHelper { } } - static String getDescriptionColumn(DescriptionLOD lod) { + static String getDescriptionColumn(DescriptionLoD lod) { switch (lod) { case FULL: return "full_description"; diff --git a/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java b/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java index 83e8801532..82e0531b46 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java @@ -22,6 +22,7 @@ import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.logging.Level; import javafx.beans.Observable; import javax.swing.Action; import org.joda.time.DateTime; @@ -29,8 +30,8 @@ import org.joda.time.DateTimeZone; import org.openide.nodes.Children; import org.openide.nodes.PropertySupport; import org.openide.nodes.Sheet; -import org.openide.util.Exceptions; import org.openide.util.lookup.Lookups; +import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.datamodel.DataModelActionsFactory; import org.sleuthkit.autopsy.datamodel.DisplayableItemNode; import org.sleuthkit.autopsy.datamodel.DisplayableItemNodeVisitor; @@ -46,6 +47,8 @@ import org.sleuthkit.datamodel.Content; */ class EventNode extends DisplayableItemNode { + private static final Logger LOGGER = Logger.getLogger(EventNode.class.getName()); + private final TimeLineEvent e; EventNode(TimeLineEvent eventById, AbstractFile file, BlackboardArtifact artifact) { @@ -75,7 +78,7 @@ class EventNode extends DisplayableItemNode { try { timePropery.setValue(getDateTimeString()); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { - Exceptions.printStackTrace(ex); + LOGGER.log(Level.SEVERE, "unexpected error setting date/time property on EventNode explorer node", ex); } }); @@ -105,7 +108,7 @@ class EventNode extends DisplayableItemNode { final List factoryActions = DataModelActionsFactory.getActions(content, artifact != null); actionsList.addAll(factoryActions); - return actionsList.toArray(new Action[0]); + return actionsList.toArray(new Action[actionsList.size()]); } @Override @@ -118,7 +121,11 @@ class EventNode extends DisplayableItemNode { throw new UnsupportedOperationException("Not supported yet."); // NON-NLS //To change body of generated methods, choose Tools | Templates. } - class TimeProperty extends PropertySupport.ReadWrite { + /** + * We use TimeProperty instead of a normal NodeProperty to correctly display + * the date/time when the user changes the timezone setting. + */ + private class TimeProperty extends PropertySupport.ReadWrite { private String value; @@ -127,7 +134,7 @@ class EventNode extends DisplayableItemNode { return false; } - public TimeProperty(String name, String displayName, String shortDescription, String value) { + TimeProperty(String name, String displayName, String shortDescription, String value) { super(name, String.class, displayName, shortDescription); setValue("suppressCustomEditor", Boolean.TRUE); // remove the "..." (editing) button NON-NLS this.value = value; diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/AbstractFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/AbstractFilter.java index 3411ade930..c5871454d8 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/AbstractFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/AbstractFilter.java @@ -18,6 +18,8 @@ */ package org.sleuthkit.autopsy.timeline.filters; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; import javafx.beans.property.SimpleBooleanProperty; /** @@ -28,9 +30,10 @@ public abstract class AbstractFilter implements Filter { private final SimpleBooleanProperty selected = new SimpleBooleanProperty(true); private final SimpleBooleanProperty disabled = new SimpleBooleanProperty(false); + private final BooleanBinding activeProperty = Bindings.and(selected, disabled.not()); @Override - public SimpleBooleanProperty getSelectedProperty() { + public SimpleBooleanProperty selectedProperty() { return selected; } @@ -64,4 +67,11 @@ public abstract class AbstractFilter implements Filter { return "[" + (isSelected() ? "x" : " ") + "]"; // NON-NLS } + public final boolean isActive() { + return activeProperty.get(); + } + + public final BooleanBinding activeProperty() { + return activeProperty; + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/CompoundFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/CompoundFilter.java index 297b0a3a1b..bf4c6a1b07 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/CompoundFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/CompoundFilter.java @@ -19,6 +19,7 @@ package org.sleuthkit.autopsy.timeline.filters; import java.util.List; +import java.util.Objects; import javafx.beans.Observable; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; @@ -71,7 +72,7 @@ public abstract class CompoundFilter extends Abstr private void addSubFilterListeners(List newSubfilters) { for (SubFilterType sf : newSubfilters) { //if a subfilter changes active state - sf.getSelectedProperty().addListener((Observable observable) -> { + sf.selectedProperty().addListener((Observable observable) -> { //set this filter acttive af any of the subfilters are active. setSelected(getSubFilters().parallelStream().anyMatch(Filter::isSelected)); }); @@ -83,10 +84,21 @@ public abstract class CompoundFilter extends Abstr return false; } for (int i = 0; i < oneFilter.getSubFilters().size(); i++) { - if (oneFilter.getSubFilters().get(i).equals(otherFilter.getSubFilters().get(i)) == false) { + final SubFilterType subFilter = oneFilter.getSubFilters().get(i); + final SubFilterType otherSubFilter = otherFilter.getSubFilters().get(i); + if (subFilter.equals(otherSubFilter) == false + || subFilter.isDisabled() != otherSubFilter.isDisabled() + || subFilter.isSelected() != otherSubFilter.isSelected()) { return false; } } return true; } + + @Override + public int hashCode() { + int hash = 3; + hash = 61 * hash + Objects.hashCode(this.subFilters); + return hash; + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/DataSourceFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/DataSourceFilter.java index b75193ddeb..2cd3f93d9e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/DataSourceFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/DataSourceFilter.java @@ -84,6 +84,4 @@ public class DataSourceFilter extends AbstractFilter { } return isSelected() == other.isSelected(); } - - } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/DescriptionFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/DescriptionFilter.java new file mode 100644 index 0000000000..5ac569c6f9 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/DescriptionFilter.java @@ -0,0 +1,117 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.timeline.filters; + +import java.util.Objects; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; + +public class DescriptionFilter extends AbstractFilter { + + private final DescriptionLoD descriptionLoD; + private final String description; + private final FilterMode filterMode; + + public FilterMode getFilterMode() { + return filterMode; + } + + public DescriptionFilter(DescriptionLoD descriptionLoD, String description, FilterMode filterMode) { + this.descriptionLoD = descriptionLoD; + this.description = description; + this.filterMode = filterMode; + } + + @Override + public DescriptionFilter copyOf() { + DescriptionFilter filterCopy = new DescriptionFilter(getDescriptionLoD(), getDescription(), getFilterMode()); + filterCopy.setSelected(isSelected()); + filterCopy.setDisabled(isDisabled()); + return filterCopy; + } + + @Override + public String getDisplayName() { + return getDescriptionLoD().getDisplayName() + ": " + getDescription(); + } + + @Override + public String getHTMLReportString() { + return getDisplayName() + getStringCheckBox(); + } + + /** + * @return the descriptionLoD + */ + public DescriptionLoD getDescriptionLoD() { + return descriptionLoD; + } + + /** + * @return the description + */ + public String getDescription() { + return description; + } + + public enum FilterMode { + + EXCLUDE("Exclude"), + INCLUDE("Include"); + + private final String displayName; + + private FilterMode(String displayName) { + this.displayName = displayName; + } + + private String getDisplayName() { + return displayName; + } + } + + @Override + public int hashCode() { + int hash = 7; + hash = 79 * hash + Objects.hashCode(this.descriptionLoD); + hash = 79 * hash + Objects.hashCode(this.description); + hash = 79 * hash + Objects.hashCode(this.filterMode); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final DescriptionFilter other = (DescriptionFilter) obj; + if (this.descriptionLoD != other.descriptionLoD) { + return false; + } + if (!Objects.equals(this.description, other.description)) { + return false; + } + if (this.filterMode != other.filterMode) { + return false; + } + return true; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/Filter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/Filter.java index 95226e9371..dc44c8912b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/Filter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/Filter.java @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.timeline.filters; +import javafx.beans.binding.BooleanBinding; import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -66,7 +67,7 @@ public interface Filter { void setSelected(Boolean act); - SimpleBooleanProperty getSelectedProperty(); + SimpleBooleanProperty selectedProperty(); /* * TODO: disabled state only affects the state of the checkboxes in the ui @@ -79,4 +80,8 @@ public interface Filter { SimpleBooleanProperty getDisabledProperty(); boolean isDisabled(); + + boolean isActive(); + + BooleanBinding activeProperty(); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/HideKnownFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/HideKnownFilter.java index b47c4d481f..f5e5b30731 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/HideKnownFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/HideKnownFilter.java @@ -33,7 +33,7 @@ public class HideKnownFilter extends AbstractFilter { public HideKnownFilter() { super(); - getSelectedProperty().set(false); + selectedProperty().set(false); } @Override diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/IntersectionFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/IntersectionFilter.java index f99249dcf0..7295a066b2 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/IntersectionFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/IntersectionFilter.java @@ -88,10 +88,4 @@ public class IntersectionFilter extends CompoundFilter { } return true; } - - @Override - public int hashCode() { - int hash = 7; - return hash; - } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/RootFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/RootFilter.java index bd115f303b..9034810ed7 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/RootFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/RootFilter.java @@ -18,6 +18,8 @@ */ package org.sleuthkit.autopsy.timeline.filters; +import java.util.Set; +import java.util.stream.Collectors; import javafx.collections.FXCollections; /** @@ -45,21 +47,46 @@ public class RootFilter extends IntersectionFilter { return hashFilter; } - public RootFilter(HideKnownFilter knownFilter, TagsFilter tagsFilter, HashHitsFilter hashFilter, TextFilter textFilter, TypeFilter typeFilter, DataSourcesFilter dataSourceFilter) { - super(FXCollections.observableArrayList(knownFilter, tagsFilter, hashFilter, textFilter, dataSourceFilter, typeFilter)); - setSelected(Boolean.TRUE); - setDisabled(false); + public RootFilter(HideKnownFilter knownFilter, TagsFilter tagsFilter, HashHitsFilter hashFilter, TextFilter textFilter, TypeFilter typeFilter, DataSourcesFilter dataSourceFilter, Set annonymousSubFilters) { + super(FXCollections.observableArrayList( + textFilter, + knownFilter, + dataSourceFilter, tagsFilter, + hashFilter, + typeFilter + )); this.knownFilter = knownFilter; this.tagsFilter = tagsFilter; this.hashFilter = hashFilter; this.textFilter = textFilter; this.typeFilter = typeFilter; this.dataSourcesFilter = dataSourceFilter; + getSubFilters().addAll(annonymousSubFilters); + setSelected(Boolean.TRUE); + setDisabled(false); } @Override public RootFilter copyOf() { - RootFilter filter = new RootFilter(knownFilter.copyOf(), tagsFilter.copyOf(), hashFilter.copyOf(), textFilter.copyOf(), typeFilter.copyOf(), dataSourcesFilter.copyOf()); + Set annonymousSubFilters = getSubFilters().stream() + .filter(subFilter -> + !(subFilter.equals(knownFilter) + || subFilter.equals(tagsFilter) + || subFilter.equals(hashFilter) + || subFilter.equals(typeFilter) + || subFilter.equals(textFilter) + || subFilter.equals(dataSourcesFilter))) + .map(Filter::copyOf) + .collect(Collectors.toSet()); + + RootFilter filter = new RootFilter( + knownFilter.copyOf(), + tagsFilter.copyOf(), + hashFilter.copyOf(), + textFilter.copyOf(), + typeFilter.copyOf(), + dataSourcesFilter.copyOf(), + annonymousSubFilters); filter.setSelected(isSelected()); filter.setDisabled(isDisabled()); return filter; @@ -67,7 +94,7 @@ public class RootFilter extends IntersectionFilter { @Override public int hashCode() { - return 3; + return super.hashCode(); } @Override diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/TextFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/TextFilter.java index c374a8c045..e907b1bd05 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/TextFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/TextFilter.java @@ -66,7 +66,7 @@ public class TextFilter extends AbstractFilter { @Override public String getHTMLReportString() { - return "text like \"" + StringUtils.defaultIfBlank(text.getValue(), "") + "\"" + getStringCheckBox(); // NON-NLS + return "LOWER(text) LIKE LOWER(\'" + StringUtils.defaultIfBlank(text.getValue(), "") + "\')" + getStringCheckBox(); // NON-NLS } @Override diff --git a/Core/src/org/sleuthkit/autopsy/timeline/filters/UnionFilter.java b/Core/src/org/sleuthkit/autopsy/timeline/filters/UnionFilter.java index e4a287f03d..07ef904e75 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/filters/UnionFilter.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/filters/UnionFilter.java @@ -33,4 +33,6 @@ abstract public class UnionFilter extends Compound public UnionFilter() { super(FXCollections.observableArrayList()); } + + } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/images/eye--minus.png b/Core/src/org/sleuthkit/autopsy/timeline/images/eye--minus.png new file mode 100644 index 0000000000..08b048eae3 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/timeline/images/eye--minus.png differ diff --git a/Core/src/org/sleuthkit/autopsy/timeline/images/eye--plus.png b/Core/src/org/sleuthkit/autopsy/timeline/images/eye--plus.png new file mode 100644 index 0000000000..4ad653156f Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/timeline/images/eye--plus.png differ diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractFXCellFactory.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractFXCellFactory.java new file mode 100644 index 0000000000..dadc14f121 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractFXCellFactory.java @@ -0,0 +1,80 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.timeline.ui; + +import java.util.function.Supplier; +import javafx.scene.control.IndexedCell; +import javafx.scene.control.ListCell; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TreeTableCell; +import javafx.scene.control.TreeTableColumn; + +/** + * an abstract base class for Cell factories. This class provides the basic + * infrustructure for implementations to be able to create similar cells for + * listview, tableviews or treetableviews via the appropriate method call. + * Implementations need only implement the abstract configureCell method in the + * same spirit as IndexedCell.updateItem + */ +public abstract class AbstractFXCellFactory { + + public TreeTableCell< X, Y> forTreeTable(TreeTableColumn< X, Y> column) { + return new AbstractTreeTableCell(); + } + + public TableCell forTable(TableColumn column) { + return new AbstractTableCell(); + } + + public ListCell< Y> forList() { + return new AbstractListCell(); + } + + protected abstract void configureCell(IndexedCell cell, Y item, boolean empty, Supplier supplier); + + private class AbstractTableCell extends TableCell { + + @Override + @SuppressWarnings({"unchecked"}) //we know it will be X but there is a flaw in getTableRow return type + protected void updateItem(Y item, boolean empty) { + super.updateItem(item, empty); + configureCell(this, item, empty, (() -> (X) this.getTableRow().getItem())); + } + } + + private class AbstractTreeTableCell extends TreeTableCell { + + @Override + protected void updateItem(Y item, boolean empty) { + super.updateItem(item, empty); + configureCell(this, item, empty, (() -> this.getTreeTableRow().getItem())); + } + } + + private class AbstractListCell extends ListCell< Y> { + + @Override + @SuppressWarnings("unchecked") //for a list X should always equal Y + protected void updateItem(Y item, boolean empty) { + super.updateItem(item, empty); + configureCell(this, item, empty, () -> (X) this.getItem()); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualization.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualization.java index 1f8a57de48..993f61501e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualization.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractVisualization.java @@ -73,7 +73,7 @@ import org.sleuthkit.autopsy.timeline.events.RefreshRequestedEvent; * {@link XYChart} doing the rendering. Is this a good idea? -jm TODO: pull up * common history context menu items out of derived classes? -jm */ -public abstract class AbstractVisualization & TimeLineChart> extends BorderPane implements TimeLineView { +public abstract class AbstractVisualization & TimeLineChart> extends BorderPane implements TimeLineView { protected final SimpleBooleanProperty hasEvents = new SimpleBooleanProperty(true); @@ -173,8 +173,8 @@ public abstract class AbstractVisualization getYAxis(); /** - * update this visualization based on current state of zoom / - * filters. Primarily this invokes the background {@link Task} returned by + * update this visualization based on current state of zoom / filters. + * Primarily this invokes the background {@link Task} returned by * {@link #getUpdateTask()} which derived classes must implement. */ synchronized public void update() { @@ -304,7 +304,7 @@ public abstract class AbstractVisualization t : tickMarks) { assignLeafLabel(new TwoPartDateTime(getTickMarkLabel(t.getValue())).leaf, diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java index 6d601e06a8..6aca0b6b6c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/VisualizationPanel.java @@ -315,14 +315,14 @@ public class VisualizationPanel extends BorderPane implements TimeLineView { public synchronized void setController(TimeLineController controller) { this.controller = controller; setModel(controller.getEventsModel()); - setViewMode(controller.getViewMode().get()); + setViewMode(controller.viewModeProperty().get()); controller.getNeedsHistogramRebuild().addListener((ObservableValue observable, Boolean oldValue, Boolean newValue) -> { if (newValue) { refreshHistorgram(); } }); - controller.getViewMode().addListener((ObservableValue ov, VisualizationMode t, VisualizationMode t1) -> { + controller.viewModeProperty().addListener((ObservableValue ov, VisualizationMode t, VisualizationMode t1) -> { setViewMode(t1); }); TimeLineController.getTimeZone().addListener(timeRangeInvalidationListener); @@ -361,7 +361,7 @@ public class VisualizationPanel extends BorderPane implements TimeLineView { } } - private synchronized void setVisualization(final AbstractVisualization newViz) { + private synchronized void setVisualization(final AbstractVisualization newViz) { Platform.runLater(() -> { synchronized (VisualizationPanel.this) { if (visualization != null) { @@ -375,7 +375,7 @@ public class VisualizationPanel extends BorderPane implements TimeLineView { visualization.setController(controller); notificationPane.setContent(visualization); if (visualization instanceof DetailViewPane) { - navPanel.setChart((DetailViewPane) visualization); + navPanel.setDetailViewPane((DetailViewPane) visualization); } visualization.hasEvents.addListener((ObservableValue observable, Boolean oldValue, Boolean newValue) -> { if (newValue == false) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AggregateEventNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AggregateEventNode.java deleted file mode 100644 index f28ec9269d..0000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/AggregateEventNode.java +++ /dev/null @@ -1,523 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2013-15 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.timeline.ui.detailview; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ExecutionException; -import java.util.logging.Level; -import java.util.stream.Collectors; -import javafx.application.Platform; -import javafx.beans.property.SimpleObjectProperty; -import javafx.event.EventHandler; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.Cursor; -import javafx.scene.control.Button; -import javafx.scene.control.ContextMenu; -import javafx.scene.control.Label; -import javafx.scene.control.OverrunStyle; -import javafx.scene.control.Tooltip; -import javafx.scene.effect.DropShadow; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.input.MouseButton; -import javafx.scene.input.MouseEvent; -import javafx.scene.layout.Background; -import javafx.scene.layout.BackgroundFill; -import javafx.scene.layout.Border; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.BorderStroke; -import javafx.scene.layout.BorderStrokeStyle; -import javafx.scene.layout.BorderWidths; -import javafx.scene.layout.CornerRadii; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Pane; -import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; -import javafx.scene.paint.Color; -import org.apache.commons.lang3.StringUtils; -import org.joda.time.DateTime; -import org.joda.time.Interval; -import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.coreutils.ColorUtilities; -import org.sleuthkit.autopsy.coreutils.LoggedTask; -import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.coreutils.ThreadConfined; -import org.sleuthkit.autopsy.timeline.TimeLineController; -import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent; -import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; -import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; -import org.sleuthkit.autopsy.timeline.filters.RootFilter; -import org.sleuthkit.autopsy.timeline.filters.TextFilter; -import org.sleuthkit.autopsy.timeline.filters.TypeFilter; -import org.sleuthkit.autopsy.timeline.zooming.DescriptionLOD; -import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; -import org.sleuthkit.datamodel.SleuthkitCase; -import org.sleuthkit.datamodel.TskCoreException; - -/** - * Represents an {@link AggregateEvent} in a {@link EventDetailChart}. - */ -public class AggregateEventNode extends StackPane { - - private static final Logger LOGGER = Logger.getLogger(AggregateEventNode.class.getName()); - - private static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png"); - private final static Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS - private final static Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS - private final static Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS - - private static final CornerRadii CORNER_RADII = new CornerRadii(3); - - /** - * the border to apply when this node is 'selected' - */ - private static final Border selectionBorder = new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, CORNER_RADII, new BorderWidths(2))); - - /** - * The event this AggregateEventNode represents visually - */ - private AggregateEvent aggEvent; - - private final AggregateEventNode parentEventNode; - - /** - * the region that represents the time span of this node's event - */ - private final Region spanRegion = new Region(); - - /** - * The label used to display this node's event's description - */ - private final Label descrLabel = new Label(); - - /** - * The label used to display this node's event count - */ - private final Label countLabel = new Label(); - - /** - * The IamgeView used to show the icon for this node's event's type - */ - private final ImageView eventTypeImageView = new ImageView(); - - /** - * Pane that contains AggregateEventNodes of any 'subevents' if they are - * displayed - * - * //TODO: move more of the control of subnodes/events here and out of - * EventDetail Chart - */ - private final Pane subNodePane = new Pane(); - - /** - * the context menu that with the slider that controls subnode/event display - * - * //TODO: move more of the control of subnodes/events here and out of - * EventDetail Chart - */ - private final SimpleObjectProperty contextMenu = new SimpleObjectProperty<>(); - - /** - * the Background used to fill the spanRegion, this varies epending on the - * selected/highlighted state of this node in its parent EventDetailChart - */ - private Background spanFill; - - private final Button plusButton = new Button(null, new ImageView(PLUS)) { - { - setMinSize(16, 16); - setMaxSize(16, 16); - setPrefSize(16, 16); - } - }; - private final Button minusButton = new Button(null, new ImageView(MINUS)) { - { - setMinSize(16, 16); - setMaxSize(16, 16); - setPrefSize(16, 16); - } - }; - private final EventDetailChart chart; - - private SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); - private DescriptionVisibility descrVis; - private final SleuthkitCase sleuthkitCase; - private final FilteredEventsModel eventsModel; - - private Tooltip tooltip; - private final ImageView hashIV = new ImageView(HASH_PIN); - private final ImageView tagIV = new ImageView(TAG); - - public AggregateEventNode(final AggregateEvent aggEvent, AggregateEventNode parentEventNode, EventDetailChart chart) { - this.aggEvent = aggEvent; - descLOD.set(aggEvent.getLOD()); - this.parentEventNode = parentEventNode; - this.chart = chart; - sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase(); - eventsModel = chart.getController().getEventsModel(); - - final Region region = new Region(); - HBox.setHgrow(region, Priority.ALWAYS); - - final HBox hBox = new HBox(descrLabel, countLabel, region, hashIV, tagIV, minusButton, plusButton); - if (aggEvent.getEventIDsWithHashHits().isEmpty()) { - hashIV.setManaged(false); - hashIV.setVisible(false); - } - if (aggEvent.getEventIDsWithTags().isEmpty()) { - tagIV.setManaged(false); - tagIV.setVisible(false); - } - hBox.setPrefWidth(USE_COMPUTED_SIZE); - hBox.setMinWidth(USE_PREF_SIZE); - hBox.setPadding(new Insets(2, 5, 2, 5)); - hBox.setAlignment(Pos.CENTER_LEFT); - - minusButton.setVisible(false); - plusButton.setVisible(false); - minusButton.setManaged(false); - plusButton.setManaged(false); - final BorderPane borderPane = new BorderPane(subNodePane, hBox, null, null, null); - BorderPane.setAlignment(subNodePane, Pos.TOP_LEFT); - borderPane.setPrefWidth(USE_COMPUTED_SIZE); - - getChildren().addAll(spanRegion, borderPane); - - setAlignment(Pos.TOP_LEFT); - setMinHeight(24); - minWidthProperty().bind(spanRegion.widthProperty()); - setPrefHeight(USE_COMPUTED_SIZE); - setMaxHeight(USE_PREF_SIZE); - - //set up subnode pane sizing contraints - subNodePane.setPrefHeight(USE_COMPUTED_SIZE); - subNodePane.setMinHeight(USE_PREF_SIZE); - subNodePane.setMinWidth(USE_PREF_SIZE); - subNodePane.setMaxHeight(USE_PREF_SIZE); - subNodePane.setMaxWidth(USE_PREF_SIZE); - subNodePane.setPickOnBounds(false); - - //setup description label - eventTypeImageView.setImage(aggEvent.getType().getFXImage()); - descrLabel.setGraphic(eventTypeImageView); - descrLabel.setPrefWidth(USE_COMPUTED_SIZE); - descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); - - descrLabel.setMouseTransparent(true); - setDescriptionVisibility(chart.getDescrVisibility().get()); - - //setup backgrounds - final Color evtColor = aggEvent.getType().getColor(); - spanFill = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)); - setBackground(new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); - setCursor(Cursor.HAND); - spanRegion.setStyle("-fx-border-width:2 0 2 2; -fx-border-radius: 2; -fx-border-color: " + ColorUtilities.getRGBCode(evtColor) + ";"); // NON-NLS - spanRegion.setBackground(spanFill); - - //set up mouse hover effect and tooltip - setOnMouseEntered((MouseEvent e) -> { - //defer tooltip creation till needed, this had a surprisingly large impact on speed of loading the chart - installTooltip(); - spanRegion.setEffect(new DropShadow(10, evtColor)); - minusButton.setVisible(true); - plusButton.setVisible(true); - minusButton.setManaged(true); - plusButton.setManaged(true); - toFront(); - }); - - setOnMouseExited((MouseEvent e) -> { - spanRegion.setEffect(null); - minusButton.setVisible(false); - plusButton.setVisible(false); - minusButton.setManaged(false); - plusButton.setManaged(false); - }); - - setOnMouseClicked(new EventMouseHandler()); - - plusButton.disableProperty().bind(descLOD.isEqualTo(DescriptionLOD.FULL)); - minusButton.disableProperty().bind(descLOD.isEqualTo(aggEvent.getLOD())); - - plusButton.setOnMouseClicked(e -> { - final DescriptionLOD next = descLOD.get().next(); - if (next != null) { - loadSubClusters(next); - descLOD.set(next); - } - }); - minusButton.setOnMouseClicked(e -> { - final DescriptionLOD previous = descLOD.get().previous(); - if (previous != null) { - loadSubClusters(previous); - descLOD.set(previous); - } - }); - } - - synchronized private void installTooltip() { - //TODO: all this work should probably go on a background thread... - if (tooltip == null) { - HashMap hashSetCounts = new HashMap<>(); - if (!aggEvent.getEventIDsWithHashHits().isEmpty()) { - hashSetCounts = new HashMap<>(); - try { - for (TimeLineEvent tle : eventsModel.getEventsById(aggEvent.getEventIDsWithHashHits())) { - Set hashSetNames = sleuthkitCase.getAbstractFileById(tle.getFileID()).getHashSetNames(); - for (String hashSetName : hashSetNames) { - hashSetCounts.merge(hashSetName, 1L, Long::sum); - } - } - } catch (TskCoreException ex) { - LOGGER.log(Level.SEVERE, "Error getting hashset hit info for event.", ex); - } - } - - Map tagCounts = new HashMap<>(); - if (!aggEvent.getEventIDsWithTags().isEmpty()) { - tagCounts.putAll( eventsModel.getTagCountsByTagName(aggEvent.getEventIDsWithTags())); - - } - - String hashSetCountsString = hashSetCounts.entrySet().stream() - .map((Map.Entry t) -> t.getKey() + " : " + t.getValue()) - .collect(Collectors.joining("\n")); - String tagCountsString = tagCounts.entrySet().stream() - .map((Map.Entry t) -> t.getKey() + " : " + t.getValue()) - .collect(Collectors.joining("\n")); - - tooltip = new Tooltip( - NbBundle.getMessage(this.getClass(), "AggregateEventNode.installTooltip.text", - getEvent().getEventIDs().size(), getEvent().getType(), getEvent().getDescription(), - getEvent().getSpan().getStart().toString(TimeLineController.getZonedFormatter()), - getEvent().getSpan().getEnd().toString(TimeLineController.getZonedFormatter())) - + (hashSetCountsString.isEmpty() ? "" : "\n\nHash Set Hits\n" + hashSetCountsString) - + (tagCountsString.isEmpty() ? "" : "\n\nTags\n" + tagCountsString) - ); - Tooltip.install(AggregateEventNode.this, tooltip); - } - } - - public Pane getSubNodePane() { - return subNodePane; - } - - synchronized public AggregateEvent getEvent() { - return aggEvent; - } - - /** - * sets the width of the {@link Region} with border and background used to - * indicate the temporal span of this aggregate event - * - * @param w - */ - public void setSpanWidth(double w) { - spanRegion.setPrefWidth(w); - spanRegion.setMaxWidth(w); - spanRegion.setMinWidth(Math.max(2, w)); - } - - /** - * - * @param w the maximum width the description label should have - */ - public void setDescriptionWidth(double w) { - descrLabel.setMaxWidth(w); - } - - /** - * @param descrVis the level of description that should be displayed - */ - @ThreadConfined(type = ThreadConfined.ThreadType.JFX) - synchronized final void setDescriptionVisibility(DescriptionVisibility descrVis) { - this.descrVis = descrVis; - final int size = aggEvent.getEventIDs().size(); - - switch (descrVis) { - case COUNT_ONLY: - descrLabel.setText(""); - countLabel.setText(String.valueOf(size)); - break; - case HIDDEN: - countLabel.setText(""); - descrLabel.setText(""); - break; - default: - case SHOWN: - String description = aggEvent.getDescription(); - description = parentEventNode != null - ? " ..." + StringUtils.substringAfter(description, parentEventNode.getEvent().getDescription()) - : description; - descrLabel.setText(description); - countLabel.setText(((size == 1) ? "" : " (" + size + ")")); // NON-NLS - break; - } - } - - /** - * apply the 'effect' to visually indicate selection - * - * @param applied true to apply the selection 'effect', false to remove it - */ - void applySelectionEffect(final boolean applied) { - Platform.runLater(() -> { - if (applied) { - setBorder(selectionBorder); - } else { - setBorder(null); - } - }); - } - - /** - * apply the 'effect' to visually indicate highlighted nodes - * - * @param applied true to apply the highlight 'effect', false to remove it - */ - synchronized void applyHighlightEffect(boolean applied) { - - if (applied) { - descrLabel.setStyle("-fx-font-weight: bold;"); // NON-NLS - spanFill = new Background(new BackgroundFill(aggEvent.getType().getColor().deriveColor(0, 1, 1, .3), CORNER_RADII, Insets.EMPTY)); - spanRegion.setBackground(spanFill); - setBackground(new Background(new BackgroundFill(aggEvent.getType().getColor().deriveColor(0, 1, 1, .2), CORNER_RADII, Insets.EMPTY))); - } else { - descrLabel.setStyle("-fx-font-weight: normal;"); // NON-NLS - spanFill = new Background(new BackgroundFill(aggEvent.getType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY)); - spanRegion.setBackground(spanFill); - setBackground(new Background(new BackgroundFill(aggEvent.getType().getColor().deriveColor(0, 1, 1, .1), CORNER_RADII, Insets.EMPTY))); - } - } - - String getDisplayedDescription() { - return descrLabel.getText(); - } - - double getLayoutXCompensation() { - return (parentEventNode != null ? parentEventNode.getLayoutXCompensation() : 0) - + getBoundsInParent().getMinX(); - } - - /** - * @return the contextMenu - */ - public ContextMenu getContextMenu() { - return contextMenu.get(); - } - - /** - * @param contextMenu the contextMenu to set - */ - public void setContextMenu(ContextMenu contextMenu) { - this.contextMenu.set(contextMenu); - } - - /** - * loads sub-clusters at the given Description LOD - * - * @param newDescriptionLOD - */ - synchronized private void loadSubClusters(DescriptionLOD newDescriptionLOD) { - getSubNodePane().getChildren().clear(); - if (newDescriptionLOD == aggEvent.getLOD()) { - chart.setRequiresLayout(true); - chart.requestChartLayout(); - } else { - RootFilter combinedFilter = eventsModel.filterProperty().get().copyOf(); - //make a new filter intersecting the global filter with text(description) and type filters to restrict sub-clusters - combinedFilter.getSubFilters().addAll(new TextFilter(aggEvent.getDescription()), - new TypeFilter(aggEvent.getType())); - - //make a new end inclusive span (to 'filter' with) - final Interval span = aggEvent.getSpan().withEndMillis(aggEvent.getSpan().getEndMillis() + 1000); - - //make a task to load the subnodes - LoggedTask> loggedTask = new LoggedTask>( - NbBundle.getMessage(this.getClass(), "AggregateEventNode.loggedTask.name"), true) { - - @Override - protected List call() throws Exception { - //query for the sub-clusters - List aggregatedEvents = eventsModel.getAggregatedEvents(new ZoomParams(span, - eventsModel.eventTypeZoomProperty().get(), - combinedFilter, - newDescriptionLOD)); - //for each sub cluster make an AggregateEventNode to visually represent it, and set x-position - return aggregatedEvents.stream().map(aggEvent -> { - AggregateEventNode subNode = new AggregateEventNode(aggEvent, AggregateEventNode.this, chart); - subNode.setLayoutX(chart.getXAxis().getDisplayPosition(new DateTime(aggEvent.getSpan().getStartMillis())) - getLayoutXCompensation()); - return subNode; - }).collect(Collectors.toList()); // return list of AggregateEventNodes representing subclusters - } - - @Override - protected void succeeded() { - try { - chart.setCursor(Cursor.WAIT); - //assign subNodes and request chart layout - getSubNodePane().getChildren().setAll(get()); - setDescriptionVisibility(descrVis); - chart.setRequiresLayout(true); - chart.requestChartLayout(); - chart.setCursor(null); - } catch (InterruptedException | ExecutionException ex) { - LOGGER.log(Level.SEVERE, "Error loading subnodes", ex); - } - } - }; - - //start task - chart.getController().monitorTask(loggedTask); - } - } - - /** - * event handler used for mouse events on {@link AggregateEventNode}s - */ - private class EventMouseHandler implements EventHandler { - - @Override - public void handle(MouseEvent t) { - if (t.getButton() == MouseButton.PRIMARY) { - t.consume(); - if (t.isShiftDown()) { - if (chart.selectedNodes.contains(AggregateEventNode.this) == false) { - chart.selectedNodes.add(AggregateEventNode.this); - } - } else if (t.isShortcutDown()) { - chart.selectedNodes.removeAll(AggregateEventNode.this); - } else if (t.getClickCount() > 1) { - final DescriptionLOD next = descLOD.get().next(); - if (next != null) { - loadSubClusters(next); - descLOD.set(next); - } - } else { - chart.selectedNodes.setAll(AggregateEventNode.this); - } - } - } - } -} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/Bundle.properties b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/Bundle.properties index 7b87db0380..091926228a 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/Bundle.properties @@ -1,12 +1,8 @@ Timeline.ui.detailview.tooltip.text={0}\nRight-click to remove.\nRight-drag to reposition. -AggregateEventNode.installTooltip.text={0} {1} events\n{2}\nbetween\t{3}\nand \t{4} -AggregateEventNode.loggedTask.name=Load sub events DetailViewPane.loggedTask.name=Update Details DetailViewPane.loggedTask.preparing=preparing DetailViewPane.loggedTask.queryDb=querying db DetailViewPane.loggedTask.updateUI=updating ui -EventDetailChart.contextMenu.zoomHistory.name=Zoom History -EventDetailChart.chartContextMenu.placeMarker.name=Place Marker DetailViewPane.truncateSliderLabel.text=max description width (px)\: DetailViewPane.advancedLayoutOptionsButtonLabel.text=Advanced Layout Options DetailViewPane.bandByTypeBox.text=Band by Type diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DescriptionVisibility.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DescriptionVisibility.java index 6689a87ed4..3f32941725 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DescriptionVisibility.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DescriptionVisibility.java @@ -22,8 +22,9 @@ package org.sleuthkit.autopsy.timeline.ui.detailview; * Level of description shown in UI NOTE: this is a separate concept form * {@link DescriptionLOD} */ -enum DescriptionVisibility { - - HIDDEN, COUNT_ONLY, SHOWN; +public enum DescriptionVisibility { + HIDDEN, + COUNT_ONLY, + SHOWN; } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index e1a0ec1867..40ea735938 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014 Basis Technology Corp. + * Copyright 2014-15 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,10 +18,9 @@ */ package org.sleuthkit.autopsy.timeline.ui.detailview; -import java.net.URL; import java.util.ArrayList; +import java.util.List; import java.util.Map; -import java.util.ResourceBundle; import java.util.concurrent.ConcurrentHashMap; import javafx.application.Platform; import javafx.beans.InvalidationListener; @@ -30,7 +29,6 @@ import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.concurrent.Task; -import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.geometry.Orientation; import javafx.scene.Cursor; @@ -63,6 +61,7 @@ import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; +import org.controlsfx.control.action.Action; import org.joda.time.DateTime; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.LoggedTask; @@ -70,60 +69,49 @@ import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.timeline.FXMLConstructor; import org.sleuthkit.autopsy.timeline.TimeLineController; -import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent; +import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.ui.AbstractVisualization; -import org.sleuthkit.autopsy.timeline.ui.countsview.CountsViewPane; -import org.sleuthkit.autopsy.timeline.ui.detailview.tree.NavTreeNode; import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; /** - * FXML Controller class for a {@link EventDetailChart} based implementation of - * a TimeLineView. + * Controller class for a {@link EventDetailChart} based implementation of a + * TimeLineView. * * This class listens to changes in the assigned {@link FilteredEventsModel} and * updates the internal {@link EventDetailChart} to reflect the currently * requested events. * - * This class captures input from the user in the form of mouse clicks on graph - * bars, and forwards them to the assigned {@link TimeLineController} - * * Concurrency Policy: Access to the private members clusterChart, dateAxis, * EventTypeMap, and dataSets is all linked directly to the ClusterChart which - * must only be manipulated on the JavaFx thread (through {@link Platform#runLater(java.lang.Runnable) - * } - * - * {@link CountsChartPane#filteredEvents} should encapsulate all needed - * synchronization internally. - * - * TODO: refactor common code out of this class and CountsChartPane into - * {@link AbstractVisualization} + * must only be manipulated on the JavaFx thread. */ -public class DetailViewPane extends AbstractVisualization { +public class DetailViewPane extends AbstractVisualization, EventDetailChart> { - private final static Logger LOGGER = Logger.getLogger(CountsViewPane.class.getName()); + + private final static Logger LOGGER = Logger.getLogger(DetailViewPane.class.getName()); - private MultipleSelectionModel> treeSelectionModel; + private MultipleSelectionModel>> treeSelectionModel; //these three could be injected from fxml but it was causing npe's private final DateAxis dateAxis = new DateAxis(); - private final Axis verticalAxis = new EventAxis(); + private final Axis verticalAxis = new EventAxis(); //private access to barchart data - private final Map> eventTypeToSeriesMap = new ConcurrentHashMap<>(); + private final Map> eventTypeToSeriesMap = new ConcurrentHashMap<>(); private final ScrollBar vertScrollBar = new ScrollBar(); private final Region region = new Region(); - private final ObservableList aggregatedEvents = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); + private final ObservableList> highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); - private final ObservableList highlightedNodes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); - - public ObservableList getAggregatedEvents() { - return aggregatedEvents; + public ObservableList> getEventBundles() { + return chart.getEventBundles(); } public DetailViewPane(Pane partPane, Pane contextPane, Region spacer) { @@ -146,16 +134,17 @@ public class DetailViewPane extends AbstractVisualization change) -> { + highlightedNodes.addListener((ListChangeListener.Change> change) -> { + while (change.next()) { - change.getAddedSubList().forEach(aeNode -> { - aeNode.applyHighlightEffect(true); + change.getAddedSubList().forEach(node -> { + node.applyHighlightEffect(true); }); - change.getRemoved().forEach(aeNode -> { - aeNode.applyHighlightEffect(false); + change.getRemoved().forEach(node -> { + node.applyHighlightEffect(false); }); } }); @@ -166,7 +155,7 @@ public class DetailViewPane extends AbstractVisualization) (ScrollEvent t) -> { + this.onScrollProperty().set((ScrollEvent t) -> { vertScrollBar.valueProperty().set(Math.max(0, Math.min(100, vertScrollBar.getValue() - t.getDeltaY() / 200.0))); }); @@ -212,8 +201,9 @@ public class DetailViewPane extends AbstractVisualization { highlightedNodes.clear(); selectedNodes.stream().forEach((tn) -> { - for (AggregateEventNode n : chart.getNodes((AggregateEventNode t) - -> t.getEvent().getDescription().equals(tn.getEvent().getDescription()))) { + + for (EventBundleNodeBase n : chart.getNodes((EventBundleNodeBase t) -> + t.getDescription().equals(tn.getDescription()))) { highlightedNodes.add(n); } }); @@ -227,17 +217,18 @@ public class DetailViewPane extends AbstractVisualization> selectionModel) { + public void setSelectionModel(MultipleSelectionModel>> selectionModel) { this.treeSelectionModel = selectionModel; treeSelectionModel.getSelectedItems().addListener((Observable observable) -> { highlightedNodes.clear(); - for (TreeItem tn : treeSelectionModel.getSelectedItems()) { - for (AggregateEventNode n : chart.getNodes((AggregateEventNode t) - -> t.getEvent().getDescription().equals(tn.getValue().getDescription()))) { + for (TreeItem> tn : treeSelectionModel.getSelectedItems()) { + + for (EventBundleNodeBase n : chart.getNodes((EventBundleNodeBase t) -> + t.getDescription().equals(tn.getValue().getDescription()))) { highlightedNodes.add(n); } } @@ -250,7 +241,7 @@ public class DetailViewPane extends AbstractVisualization getYAxis() { + protected Axis getYAxis() { return verticalAxis; } @@ -279,15 +270,13 @@ public class DetailViewPane extends AbstractVisualization getSeries(final EventType et) { - XYChart.Series series = eventTypeToSeriesMap.get(et); - if (series == null) { - series = new XYChart.Series<>(); + private XYChart.Series getSeries(final EventType et) { + return eventTypeToSeriesMap.computeIfAbsent(et, (EventType t) -> { + XYChart.Series series = new XYChart.Series<>(); series.setName(et.getDisplayName()); - eventTypeToSeriesMap.put(et, series); dataSets.add(series); - } - return series; + return series; + }); } @Override @@ -300,11 +289,12 @@ public class DetailViewPane extends AbstractVisualization { - if (isCancelled() == false) { + + if (isCancelled() == false) { + Platform.runLater(() -> { setCursor(Cursor.WAIT); - } - }); + }); + } updateProgress(-1, 1); updateMessage(NbBundle.getMessage(this.getClass(), "DetailViewPane.loggedTask.preparing")); @@ -314,7 +304,6 @@ public class DetailViewPane extends AbstractVisualization { if (isCancelled()) { @@ -326,21 +315,24 @@ public class DetailViewPane extends AbstractVisualization eventClusters = filteredEvents.getEventClusters(); + + final int size = eventClusters.size(); + for (int i = 0; i < size; i++) { if (isCancelled()) { break; } - updateProgress(i++, size); + final EventCluster cluster = eventClusters.get(i); + updateProgress(i, size); updateMessage(NbBundle.getMessage(this.getClass(), "DetailViewPane.loggedTask.updateUI")); - final XYChart.Data xyData = new BarChart.Data<>(new DateTime(e.getSpan().getStartMillis()), e); + final XYChart.Data xyData = new BarChart.Data<>(new DateTime(cluster.getSpan().getStartMillis()), cluster); - Platform.runLater(() -> { - if (isCancelled() == false) { - getSeries(e.getType()).getData().add(xyData); - } - }); + if (isCancelled() == false) { + Platform.runLater(() -> { + getSeries(cluster.getEventType()).getData().add(xyData); + }); + } } Platform.runLater(() -> { @@ -348,19 +340,20 @@ public class DetailViewPane extends AbstractVisualization c1, Boolean selected) { + c1.applySelectionEffect(selected); } private class DetailViewSettingsPane extends HBox { @@ -377,12 +370,6 @@ public class DetailViewPane extends AbstractVisualization { @@ -450,11 +437,11 @@ public class DetailViewPane extends AbstractVisualization { if (newToggle == countsRadio) { - chart.getDescrVisibility().set(DescriptionVisibility.COUNT_ONLY); + chart.descrVisibilityProperty().set(DescriptionVisibility.COUNT_ONLY); } else if (newToggle == showRadio) { - chart.getDescrVisibility().set(DescriptionVisibility.SHOWN); + chart.descrVisibilityProperty().set(DescriptionVisibility.SHOWN); } else if (newToggle == hiddenRadio) { - chart.getDescrVisibility().set(DescriptionVisibility.HIDDEN); + chart.descrVisibilityProperty().set(DescriptionVisibility.HIDDEN); } }); @@ -480,7 +467,15 @@ public class DetailViewPane extends AbstractVisualization - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java index 486f9f290e..89b4de0ec7 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventAxis.java @@ -22,21 +22,21 @@ import java.util.Collections; import java.util.List; import javafx.scene.chart.Axis; import javafx.scene.chart.XYChart; -import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; /** * No-Op axis that doesn't do anything usefull but is necessary to pass * AggregateEvent as the second member of {@link XYChart.Data} objects */ -class EventAxis extends Axis { +class EventAxis extends Axis { @Override - public double getDisplayPosition(AggregateEvent value) { + public double getDisplayPosition(EventCluster value) { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } @Override - public AggregateEvent getValueForDisplay(double displayPosition) { + public EventCluster getValueForDisplay(double displayPosition) { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } @@ -46,17 +46,17 @@ class EventAxis extends Axis { } @Override - public boolean isValueOnAxis(AggregateEvent value) { + public boolean isValueOnAxis(EventCluster value) { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } @Override - public double toNumericValue(AggregateEvent value) { + public double toNumericValue(EventCluster value) { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } @Override - public AggregateEvent toRealValue(double value) { + public EventCluster toRealValue(double value) { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } @@ -66,7 +66,7 @@ class EventAxis extends Axis { } @Override - protected List calculateTickValues(double length, Object range) { + protected List calculateTickValues(double length, Object range) { return Collections.emptyList(); } @@ -76,7 +76,7 @@ class EventAxis extends Axis { } @Override - protected String getTickMarkLabel(AggregateEvent value) { + protected String getTickMarkLabel(EventCluster value) { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java new file mode 100644 index 0000000000..ee57b6fe0d --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventBundleNodeBase.java @@ -0,0 +1,337 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.timeline.ui.detailview; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import java.util.stream.Collectors; +import javafx.beans.Observable; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; +import javafx.concurrent.Task; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; +import javafx.scene.effect.DropShadow; +import javafx.scene.effect.Effect; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.Border; +import javafx.scene.layout.BorderStroke; +import javafx.scene.layout.BorderStrokeStyle; +import javafx.scene.layout.BorderWidths; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; +import static javafx.scene.layout.Region.USE_COMPUTED_SIZE; +import static javafx.scene.layout.Region.USE_PREF_SIZE; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import org.joda.time.DateTime; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; +import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; +import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; +import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; +import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; +import static org.sleuthkit.autopsy.timeline.ui.detailview.EventBundleNodeBase.show; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * + */ +public abstract class EventBundleNodeBase, ParentType extends EventBundle, ParentNodeType extends EventBundleNodeBase> extends StackPane { + + private static final Logger LOGGER = Logger.getLogger(EventBundleNodeBase.class.getName()); + private static final Image HASH_PIN = new Image("/org/sleuthkit/autopsy/images/hashset_hits.png"); //NOI18N + private static final Image TAG = new Image("/org/sleuthkit/autopsy/images/green-tag-icon-16.png"); // NON-NLS //NOI18N + + static final CornerRadii CORNER_RADII_3 = new CornerRadii(3); + static final CornerRadii CORNER_RADII_1 = new CornerRadii(1); + + private final Border SELECTION_BORDER; + private static final Map dropShadowMap = new ConcurrentHashMap<>(); + + static void configureLoDButton(Button b) { + b.setMinSize(16, 16); + b.setMaxSize(16, 16); + b.setPrefSize(16, 16); + show(b, false); + } + + static void show(Node b, boolean show) { + b.setVisible(show); + b.setManaged(show); + } + + protected final EventDetailChart chart; + final SimpleObjectProperty descLOD = new SimpleObjectProperty<>(); + final SimpleObjectProperty descVisibility = new SimpleObjectProperty<>(DescriptionVisibility.SHOWN); + protected final BundleType eventBundle; + + protected final ParentNodeType parentNode; + + final SleuthkitCase sleuthkitCase; + final FilteredEventsModel eventsModel; + + final Background highlightedBackground; + final Background defaultBackground; + final Color evtColor; + + final List subNodes = new ArrayList<>(); + final Pane subNodePane = new Pane(); + final Label descrLabel = new Label(); + final Label countLabel = new Label(); + + final ImageView hashIV = new ImageView(HASH_PIN); + final ImageView tagIV = new ImageView(TAG); + final HBox infoHBox = new HBox(5, descrLabel, countLabel, hashIV, tagIV); + + private Tooltip tooltip; + + public EventBundleNodeBase(EventDetailChart chart, BundleType eventBundle, ParentNodeType parentNode) { + this.eventBundle = eventBundle; + this.parentNode = parentNode; + this.chart = chart; + + this.descLOD.set(eventBundle.getDescriptionLoD()); + sleuthkitCase = chart.getController().getAutopsyCase().getSleuthkitCase(); + eventsModel = chart.getController().getEventsModel(); + evtColor = getEventType().getColor(); + defaultBackground = new Background(new BackgroundFill(evtColor.deriveColor(0, 1, 1, .1), CORNER_RADII_3, Insets.EMPTY)); + highlightedBackground = new Background(new BackgroundFill(evtColor.deriveColor(0, 1.1, 1.1, .3), CORNER_RADII_3, Insets.EMPTY)); + SELECTION_BORDER = new Border(new BorderStroke(evtColor.darker().desaturate(), BorderStrokeStyle.SOLID, CORNER_RADII_3, new BorderWidths(2))); + if (eventBundle.getEventIDsWithHashHits().isEmpty()) { + show(hashIV, false); + } + if (eventBundle.getEventIDsWithTags().isEmpty()) { + show(tagIV, false); + } + + setBackground(defaultBackground); + setAlignment(Pos.TOP_LEFT); + + setPrefHeight(USE_COMPUTED_SIZE); + heightProperty().addListener((Observable observable) -> { + chart.layoutPlotChildren(); + }); + setMaxHeight(USE_PREF_SIZE); + setMaxWidth(USE_PREF_SIZE); + setLayoutX(chart.getXAxis().getDisplayPosition(new DateTime(eventBundle.getStartMillis())) - getLayoutXCompensation()); + + //initialize info hbox + infoHBox.setMinWidth(USE_PREF_SIZE); + infoHBox.setMaxWidth(USE_PREF_SIZE); + infoHBox.setPadding(new Insets(2, 5, 2, 5)); + infoHBox.setAlignment(Pos.TOP_LEFT); + infoHBox.setPickOnBounds(true); + + //set up subnode pane sizing contraints + subNodePane.setPrefHeight(USE_COMPUTED_SIZE); + subNodePane.setMaxHeight(USE_PREF_SIZE); + subNodePane.setPrefWidth(USE_COMPUTED_SIZE); + subNodePane.setMinWidth(USE_PREF_SIZE); + subNodePane.setMaxWidth(USE_PREF_SIZE); + + //set up mouse hover effect and tooltip + setOnMouseEntered((MouseEvent e) -> { + /* + * defer tooltip creation till needed, this had a surprisingly large + * impact on speed of loading the chart + */ + installTooltip(); + showHoverControls(true); + toFront(); + }); + setOnMouseExited((MouseEvent event) -> { + showHoverControls(false); + if (parentNode != null) { + parentNode.showHoverControls(true); + } + }); + + setDescriptionVisibility(DescriptionVisibility.SHOWN); + descVisibility.addListener((ObservableValue observable, DescriptionVisibility oldValue, DescriptionVisibility newValue) -> { + setDescriptionVisibility(newValue); + }); + } + + final DescriptionLoD getDescriptionLoD() { + return descLOD.get(); + } + + public final BundleType getEventBundle() { + return eventBundle; + } + + final double getLayoutXCompensation() { + return parentNode != null + ? chart.getXAxis().getDisplayPosition(new DateTime(parentNode.getStartMillis())) + : 0; + } + + @NbBundle.Messages({"# {0} - counts", + "# {1} - event type", + "# {2} - description", + "# {3} - start date/time", + "# {4} - end date/time", + "EventBundleNodeBase.tooltip.text={0} {1} events\n{2}\nbetween\t{3}\nand \t{4}"}) + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private void installTooltip() { + if (tooltip == null) { + final Task tooltTipTask = new Task() { + + @Override + protected String call() throws Exception { + HashMap hashSetCounts = new HashMap<>(); + if (eventBundle.getEventIDsWithHashHits().isEmpty() == false) { + try { + //TODO:push this to DB + for (TimeLineEvent tle : eventsModel.getEventsById(eventBundle.getEventIDsWithHashHits())) { + Set hashSetNames = sleuthkitCase.getAbstractFileById(tle.getFileID()).getHashSetNames(); + for (String hashSetName : hashSetNames) { + hashSetCounts.merge(hashSetName, 1L, Long::sum); + } + } + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "Error getting hashset hit info for event.", ex); + } + } + String hashSetCountsString = hashSetCounts.entrySet().stream() + .map((Map.Entry t) -> t.getKey() + " : " + t.getValue()) + .collect(Collectors.joining("\n")); + + Map tagCounts = new HashMap<>(); + if (eventBundle.getEventIDsWithTags().isEmpty() == false) { + tagCounts.putAll(eventsModel.getTagCountsByTagName(eventBundle.getEventIDsWithTags())); + } + String tagCountsString = tagCounts.entrySet().stream() + .map((Map.Entry t) -> t.getKey() + " : " + t.getValue()) + .collect(Collectors.joining("\n")); + + return Bundle.EventBundleNodeBase_tooltip_text(getEventIDs().size(), getEventType(), getDescription(), + TimeLineController.getZonedFormatter().print(getStartMillis()), + TimeLineController.getZonedFormatter().print(getEndMillis() + 1000)) + + (hashSetCountsString.isEmpty() ? "" : "\n\nHash Set Hits\n" + hashSetCountsString) + + (tagCountsString.isEmpty() ? "" : "\n\nTags\n" + tagCountsString); + } + + @Override + protected void succeeded() { + super.succeeded(); + try { + tooltip = new Tooltip(get()); + tooltip.setAutoHide(true); + Tooltip.install(EventBundleNodeBase.this, tooltip); + } catch (InterruptedException | ExecutionException ex) { + LOGGER.log(Level.SEVERE, "Tooltip generation failed.", ex); + Tooltip.uninstall(EventBundleNodeBase.this, tooltip); + tooltip = null; + } + } + }; + + chart.getController().monitorTask(tooltTipTask); + } + } + + /** + * apply the 'effect' to visually indicate selection + * + * @param applied true to apply the selection 'effect', false to remove it + */ + public void applySelectionEffect(boolean applied) { + setBorder(applied ? SELECTION_BORDER : null); + } + + /** + * apply the 'effect' to visually indicate highlighted nodes + * + * @param applied true to apply the highlight 'effect', false to remove it + */ + abstract void applyHighlightEffect(boolean applied); + + @SuppressWarnings("unchecked") + public List getSubNodes() { + return subNodes; + } + + abstract void setDescriptionVisibility(DescriptionVisibility get); + + void showHoverControls(final boolean showControls) { + Effect dropShadow = dropShadowMap.computeIfAbsent(getEventType(), + eventType -> new DropShadow(-10, eventType.getColor())); + setEffect(showControls ? dropShadow : null); + if (parentNode != null) { + parentNode.showHoverControls(false); + } + } + + final EventType getEventType() { + return getEventBundle().getEventType(); + } + + final String getDescription() { + return getEventBundle().getDescription(); + } + + final long getStartMillis() { + return getEventBundle().getStartMillis(); + } + + final long getEndMillis() { + return getEventBundle().getEndMillis(); + } + + final Set getEventIDs() { + return getEventBundle().getEventIDs(); + } + + @Override + protected void layoutChildren() { + chart.layoutEventBundleNodes(subNodes, 0); + super.layoutChildren(); + } + + /** + * @param w the maximum width the description label should have + */ + abstract void setDescriptionWidth(double w); + + void setDescriptionVisibilityLevel(DescriptionVisibility get) { + descVisibility.set(get); + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java new file mode 100644 index 0000000000..f41be888ce --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventClusterNode.java @@ -0,0 +1,332 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-15 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.timeline.ui.detailview; + +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import static java.util.Objects.nonNull; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import java.util.stream.Collectors; +import javafx.beans.binding.Bindings; +import javafx.concurrent.Task; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.geometry.Pos; +import javafx.scene.Cursor; +import javafx.scene.control.Button; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.SeparatorMenuItem; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Border; +import javafx.scene.layout.BorderStroke; +import javafx.scene.layout.BorderStrokeStyle; +import javafx.scene.layout.BorderWidths; +import javafx.scene.layout.VBox; +import org.controlsfx.control.action.Action; +import org.controlsfx.control.action.ActionUtils; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; +import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; +import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter; +import org.sleuthkit.autopsy.timeline.filters.RootFilter; +import org.sleuthkit.autopsy.timeline.filters.TypeFilter; +import static org.sleuthkit.autopsy.timeline.ui.detailview.EventBundleNodeBase.configureLoDButton; +import static org.sleuthkit.autopsy.timeline.ui.detailview.EventBundleNodeBase.show; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; +import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; +import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; + +/** + * + */ +final public class EventClusterNode extends EventBundleNodeBase { + + private static final Logger LOGGER = Logger.getLogger(EventClusterNode.class.getName()); + private static final BorderWidths CLUSTER_BORDER_WIDTHS = new BorderWidths(2, 1, 2, 1); + private static final Image PLUS = new Image("/org/sleuthkit/autopsy/timeline/images/plus-button.png"); // NON-NLS //NOI18N + private static final Image MINUS = new Image("/org/sleuthkit/autopsy/timeline/images/minus-button.png"); // NON-NLS //NOI18N + private final Border clusterBorder = new Border(new BorderStroke(evtColor.deriveColor(0, 1, 1, .4), BorderStrokeStyle.SOLID, CORNER_RADII_1, CLUSTER_BORDER_WIDTHS)); + + final Button plusButton = ActionUtils.createButton(new ExpandClusterAction(), ActionUtils.ActionTextBehavior.HIDE); + final Button minusButton = ActionUtils.createButton(new CollapseClusterAction(), ActionUtils.ActionTextBehavior.HIDE); + + public EventClusterNode(EventDetailChart chart, EventCluster eventCluster, EventStripeNode parentNode) { + super(chart, eventCluster, parentNode); + setMinHeight(24); + + subNodePane.setBorder(clusterBorder); + subNodePane.setBackground(defaultBackground); + subNodePane.setMaxHeight(USE_COMPUTED_SIZE); + subNodePane.setMaxWidth(USE_PREF_SIZE); + subNodePane.setMinWidth(1); + + setCursor(Cursor.HAND); + setOnMouseClicked(new MouseClickHandler()); + + configureLoDButton(plusButton); + configureLoDButton(minusButton); + + setAlignment(Pos.CENTER_LEFT); + infoHBox.getChildren().addAll(minusButton, plusButton); + getChildren().addAll(subNodePane, infoHBox); + + } + + @Override + void showHoverControls(final boolean showControls) { + super.showHoverControls(showControls); + show(plusButton, showControls); + show(minusButton, showControls); + } + + @Override + void applyHighlightEffect(boolean applied) { +// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + void setDescriptionWidth(double max) { +// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public void setDescriptionVisibility(DescriptionVisibility descrVis) { + final int size = getEventBundle().getEventIDs().size(); + switch (descrVis) { + case HIDDEN: + countLabel.setText(""); + descrLabel.setText(""); + break; + case COUNT_ONLY: + descrLabel.setText(""); + countLabel.setText(String.valueOf(size)); + break; + default: + case SHOWN: + countLabel.setText(String.valueOf(size)); + break; + } + } + + /** + * loads sub-bundles at the given Description LOD, continues + * + * @param requestedDescrLoD + * @param expand + */ + @NbBundle.Messages(value = "EventStripeNode.loggedTask.name=Load sub clusters") + private synchronized void loadSubBundles(DescriptionLoD.RelativeDetail relativeDetail) { + chart.setCursor(Cursor.WAIT); + chart.getEventBundles().removeIf(bundle -> + subNodes.stream().anyMatch(subNode -> + bundle.equals(subNode.getEventStripe())) + ); + subNodes.clear(); + + /* + * make new ZoomParams to query with + * + * We need to extend end time because for the query by one second, + * because it is treated as an open interval but we want to include + * events at exactly the time of the last event in this cluster + */ + final RootFilter subClusterFilter = getSubClusterFilter(); + final Interval subClusterSpan = new Interval(getStartMillis(), getEndMillis() + 1000); + final EventTypeZoomLevel eventTypeZoomLevel = eventsModel.eventTypeZoomProperty().get(); + final ZoomParams zoomParams = new ZoomParams(subClusterSpan, eventTypeZoomLevel, subClusterFilter, getDescriptionLoD()); + + Task> loggedTask = new Task>() { + + private volatile DescriptionLoD loadedDescriptionLoD = getDescriptionLoD().withRelativeDetail(relativeDetail); + + { + updateTitle(Bundle.EventStripeNode_loggedTask_name()); + } + + @Override + protected Collection call() throws Exception { + Collection bundles; + DescriptionLoD next = loadedDescriptionLoD; + do { + loadedDescriptionLoD = next; + if (loadedDescriptionLoD == getEventBundle().getDescriptionLoD()) { + return Collections.emptySet(); + } + bundles = eventsModel.getEventClusters(zoomParams.withDescrLOD(loadedDescriptionLoD)).stream() + .collect(Collectors.toMap(EventCluster::getDescription, //key + (eventCluster) -> new EventStripe(eventCluster, getEventCluster()), //value + EventStripe::merge) //merge method + ).values(); + next = loadedDescriptionLoD.withRelativeDetail(relativeDetail); + } while (bundles.size() == 1 && nonNull(next)); + + // return list of AbstractEventStripeNodes representing sub-bundles + return bundles; + + } + + @Override + protected void succeeded() { + + try { + Collection bundles = get(); + + if (bundles.isEmpty()) { + subNodePane.getChildren().clear(); + getChildren().setAll(subNodePane, infoHBox); + descLOD.set(getEventBundle().getDescriptionLoD()); + } else { + chart.getEventBundles().addAll(bundles); + subNodes.addAll(bundles.stream() + .map(EventClusterNode.this::createStripeNode) + .sorted(Comparator.comparing(EventStripeNode::getStartMillis)) + .collect(Collectors.toList())); + subNodePane.getChildren().setAll(subNodes); + getChildren().setAll(new VBox(infoHBox, subNodePane)); + descLOD.set(loadedDescriptionLoD); + } + } catch (InterruptedException | ExecutionException ex) { + LOGGER.log(Level.SEVERE, "Error loading subnodes", ex); + } + chart.layoutPlotChildren(); + chart.setCursor(null); + } + }; + + //start task + chart.getController().monitorTask(loggedTask); + } + + private EventStripeNode createStripeNode(EventStripe stripe) { + return new EventStripeNode(chart, stripe, this); + } + + EventCluster getEventCluster() { + return getEventBundle(); + } + + @Override + protected void layoutChildren() { + double chartX = chart.getXAxis().getDisplayPosition(new DateTime(getStartMillis())); + double w = chart.getXAxis().getDisplayPosition(new DateTime(getEndMillis())) - chartX; + subNodePane.setPrefWidth(w); + subNodePane.setMinWidth(Math.max(1, w)); + super.layoutChildren(); + } + + /** + * make a new filter intersecting the global filter with description and + * type filters to restrict sub-clusters + * + */ + RootFilter getSubClusterFilter() { + RootFilter subClusterFilter = eventsModel.filterProperty().get().copyOf(); + subClusterFilter.getSubFilters().addAll( + new DescriptionFilter(getEventBundle().getDescriptionLoD(), getDescription(), DescriptionFilter.FilterMode.INCLUDE), + new TypeFilter(getEventType())); + return subClusterFilter; + } + + /** + * event handler used for mouse events on {@link EventStripeNode}s + */ + private class MouseClickHandler implements EventHandler { + + private ContextMenu contextMenu; + + @Override + public void handle(MouseEvent t) { + + if (t.getButton() == MouseButton.PRIMARY) { + + if (t.isShiftDown()) { + if (chart.selectedNodes.contains(EventClusterNode.this) == false) { + chart.selectedNodes.add(EventClusterNode.this); + } + } else if (t.isShortcutDown()) { + chart.selectedNodes.removeAll(EventClusterNode.this); + } else if (t.getClickCount() > 1) { + final DescriptionLoD next = descLOD.get().moreDetailed(); + if (next != null) { + loadSubBundles(DescriptionLoD.RelativeDetail.MORE); + } + } else { + chart.selectedNodes.setAll(EventClusterNode.this); + } + t.consume(); + } else if (t.getButton() == MouseButton.SECONDARY) { + ContextMenu chartContextMenu = chart.getChartContextMenu(t); + if (contextMenu == null) { + contextMenu = new ContextMenu(); + contextMenu.setAutoHide(true); + + contextMenu.getItems().add(ActionUtils.createMenuItem(new ExpandClusterAction())); + contextMenu.getItems().add(ActionUtils.createMenuItem(new CollapseClusterAction())); + + contextMenu.getItems().add(new SeparatorMenuItem()); + contextMenu.getItems().addAll(chartContextMenu.getItems()); + } + contextMenu.show(EventClusterNode.this, t.getScreenX(), t.getScreenY()); + t.consume(); + } + } + } + + private class ExpandClusterAction extends Action { + + @NbBundle.Messages(value = "ExpandClusterAction.text=Expand") + ExpandClusterAction() { + super(Bundle.ExpandClusterAction_text()); + + setGraphic(new ImageView(PLUS)); + setEventHandler((ActionEvent t) -> { + final DescriptionLoD next = descLOD.get().moreDetailed(); + if (next != null) { + loadSubBundles(DescriptionLoD.RelativeDetail.MORE); + } + }); + disabledProperty().bind(descLOD.isEqualTo(DescriptionLoD.FULL)); + } + } + + private class CollapseClusterAction extends Action { + + @NbBundle.Messages(value = "CollapseClusterAction.text=Collapse") + CollapseClusterAction() { + super(Bundle.CollapseClusterAction_text()); + + setGraphic(new ImageView(MINUS)); + setEventHandler((ActionEvent t) -> { + final DescriptionLoD previous = descLOD.get().lessDetailed(); + if (previous != null) { + loadSubBundles(DescriptionLoD.RelativeDetail.LESS); + } + }); + disabledProperty().bind(Bindings.createBooleanBinding(() -> nonNull(getEventCluster()) && descLOD.get() == getEventCluster().getDescriptionLoD(), descLOD)); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java index 9d8ba43e70..1114cf6a72 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventDetailChart.java @@ -18,17 +18,20 @@ */ package org.sleuthkit.autopsy.timeline.ui.detailview; -import com.google.common.collect.Collections2; -import java.util.ArrayList; +import com.google.common.collect.Range; +import com.google.common.collect.TreeRangeMap; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.TreeMap; +import java.util.MissingResourceException; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; @@ -41,15 +44,11 @@ import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; -import javafx.collections.MapChangeListener; import javafx.collections.ObservableList; -import javafx.collections.ObservableMap; import javafx.event.ActionEvent; -import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.scene.Cursor; import javafx.scene.Group; -import javafx.scene.Node; import javafx.scene.chart.Axis; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; @@ -61,7 +60,7 @@ import javafx.scene.input.MouseEvent; import javafx.scene.shape.Line; import javafx.scene.shape.StrokeLineCap; import javafx.util.Duration; -import javax.annotation.concurrent.GuardedBy; +import org.apache.commons.lang3.tuple.ImmutablePair; import org.controlsfx.control.action.Action; import org.controlsfx.control.action.ActionGroup; import org.controlsfx.control.action.ActionUtils; @@ -71,10 +70,15 @@ import org.openide.util.NbBundle; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.actions.Back; import org.sleuthkit.autopsy.timeline.actions.Forward; -import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent; +import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; +import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; +import org.sleuthkit.autopsy.timeline.filters.AbstractFilter; +import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter; import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; +import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; /** * Custom implementation of {@link XYChart} to graph events on a horizontal @@ -90,19 +94,14 @@ import org.sleuthkit.autopsy.timeline.ui.TimeLineChart; * * //TODO: refactor the projected lines to a separate class. -jm */ -public final class EventDetailChart extends XYChart implements TimeLineChart { +public final class EventDetailChart extends XYChart implements TimeLineChart { + private static final Image HIDE = new Image("/org/sleuthkit/autopsy/timeline/images/eye--minus.png"); // NON-NLS + private static final Image SHOW = new Image("/org/sleuthkit/autopsy/timeline/images/eye--plus.png"); // NON-NLS + private static final Image MARKER = new Image("/org/sleuthkit/autopsy/timeline/images/marker.png", 16, 16, true, true, true); private static final int PROJECTED_LINE_Y_OFFSET = 5; - private static final int PROJECTED_LINE_STROKE_WIDTH = 5; - - /** - * true == layout each event type in its own band, false == mix all the - * events together during layout - */ - private final SimpleBooleanProperty bandByType = new SimpleBooleanProperty(false); - - // I don't like having these package visible, but it was the easiest way to + private static final int MINIMUM_EVENT_NODE_GAP = 4; private ContextMenu chartContextMenu; private TimeLineController controller; @@ -110,12 +109,7 @@ public final class EventDetailChart extends XYChart im private FilteredEventsModel filteredEvents; /** - * how much detail of the description to show in the ui - */ - private final SimpleObjectProperty descrVisibility = new SimpleObjectProperty<>(DescriptionVisibility.SHOWN); - - /** - * a user position-able vertical line to help the compare events + * a user positionable vertical line to help compare events */ private Line guideLine; @@ -127,41 +121,41 @@ public final class EventDetailChart extends XYChart im private IntervalSelector intervalSelector; /** - * listener that triggers layout pass + * listener that triggers chart layout pass */ - private final InvalidationListener layoutInvalidationListener = ( - Observable o) -> { - synchronized (EventDetailChart.this) { - requiresLayout = true; - requestChartLayout(); - } - }; + private final InvalidationListener layoutInvalidationListener = (Observable o) -> { + layoutPlotChildren(); + }; /** * the maximum y value used so far during the most recent layout pass */ private final ReadOnlyDoubleWrapper maxY = new ReadOnlyDoubleWrapper(0.0); + final ObservableList> selectedNodes; /** * the group that all event nodes are added to. This facilitates scrolling * by allowing a single translation of this group. */ private final Group nodeGroup = new Group(); + private final ObservableList> bundles = FXCollections.observableArrayList(); + private final Map, EventStripe> stripeDescMap = new HashMap<>(); + private final Map stripeNodeMap = new HashMap<>(); + private final Map projectionMap = new HashMap<>(); /** - * map from event to node + * list of series of data added to this chart + * + * TODO: replace this with a map from name to series? -jm */ - private final Map nodeMap = new TreeMap<>(( - AggregateEvent o1, - AggregateEvent o2) -> { - int comp = Long.compare(o1.getSpan().getStartMillis(), o2.getSpan().getStartMillis()); - if (comp != 0) { - return comp; - } else { - return Comparator.comparing(AggregateEvent::hashCode).compare(o1, o2); - } - }); + private final ObservableList> seriesList = + FXCollections.>observableArrayList(); + /** + * true == layout each event type in its own band, false == mix all the + * events together during layout + */ + private final SimpleBooleanProperty bandByType = new SimpleBooleanProperty(false); /** * true == enforce that no two events can share the same 'row', leading to * sparser but possibly clearer layout. false == put unrelated events in the @@ -169,28 +163,11 @@ public final class EventDetailChart extends XYChart im */ private final SimpleBooleanProperty oneEventPerRow = new SimpleBooleanProperty(false); - private final ObservableMap projectionMap = FXCollections.observableHashMap(); - /** - * flag indicating whether this chart actually needs a layout pass + * how much detail of the description to show in the ui */ - @GuardedBy(value = "this") - private boolean requiresLayout = true; - - final ObservableList selectedNodes; - - /** - * list of series of data added to this chart TODO: replace this with a map - * from name to series? -jm - */ - private final ObservableList> seriesList - = FXCollections.>observableArrayList(); - - private final ObservableList> sortedSeriesList = seriesList - .sorted((s1, s2) -> { - final List collect = EventType.allTypes.stream().map(EventType::getDisplayName).collect(Collectors.toList()); - return Integer.compare(collect.indexOf(s1.getName()), collect.indexOf(s2.getName())); - }); + private final SimpleObjectProperty descrVisibility = + new SimpleObjectProperty<>(DescriptionVisibility.SHOWN); /** * true == truncate all the labels to the greater of the size of their @@ -198,129 +175,84 @@ public final class EventDetailChart extends XYChart im * the labels, alow them to extend past the timespan indicator and off the * edge of the screen */ - private final SimpleBooleanProperty truncateAll = new SimpleBooleanProperty(false); + final SimpleBooleanProperty truncateAll = new SimpleBooleanProperty(false); /** * the width to truncate all labels to if truncateAll is true. adjustable * via slider if truncateAll is true */ - private final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0); + final SimpleDoubleProperty truncateWidth = new SimpleDoubleProperty(200.0); - EventDetailChart(DateAxis dateAxis, final Axis verticalAxis, ObservableList selectedNodes) { + EventDetailChart(DateAxis dateAxis, final Axis verticalAxis, ObservableList> selectedNodes) { super(dateAxis, verticalAxis); dateAxis.setAutoRanging(false); - //yAxis.setVisible(false);//TODO: why doesn't this hide the vertical axis, instead we have to turn off all parts individually? -jm + verticalAxis.setVisible(false);//TODO: why doesn't this hide the vertical axis, instead we have to turn off all parts individually? -jm verticalAxis.setTickLabelsVisible(false); verticalAxis.setTickMarkVisible(false); - setLegendVisible(false); + setPadding(Insets.EMPTY); setAlternativeColumnFillVisible(true); //all nodes are added to nodeGroup to facilitate scrolling rather than to getPlotChildren() directly getPlotChildren().add(nodeGroup); - //bind listener to events that should trigger layout - widthProperty().addListener(layoutInvalidationListener); - heightProperty().addListener(layoutInvalidationListener); -// boundsInLocalProperty().addListener(layoutInvalidationListener); + //add listener for events that should trigger layout bandByType.addListener(layoutInvalidationListener); oneEventPerRow.addListener(layoutInvalidationListener); truncateAll.addListener(layoutInvalidationListener); truncateWidth.addListener(layoutInvalidationListener); descrVisibility.addListener(layoutInvalidationListener); - //this is needed to allow non circular binding of the guideline and timerangRect heights to the height of the chart + //this is needed to allow non circular binding of the guideline and timerangeRect heights to the height of the chart + //TODO: seems like a hack, can we remove? -jm boundsInLocalProperty().addListener((Observable observable) -> { setPrefHeight(boundsInLocalProperty().get().getHeight()); }); - //set up mouse listeners - final EventHandler clickHandler = (MouseEvent clickEvent) -> { + ///////set up mouse listeners + setOnMouseClicked((MouseEvent clickEvent) -> { if (chartContextMenu != null) { chartContextMenu.hide(); } if (clickEvent.getButton() == MouseButton.SECONDARY && clickEvent.isStillSincePress()) { - - chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(new Action( - NbBundle.getMessage(this.getClass(), "EventDetailChart.chartContextMenu.placeMarker.name")) { - { - setGraphic(new ImageView(new Image("/org/sleuthkit/autopsy/timeline/images/marker.png", 16, 16, true, true, true))); // NON-NLS - setEventHandler((ActionEvent t) -> { - if (guideLine == null) { - guideLine = new GuideLine(0, 0, 0, getHeight(), dateAxis); - guideLine.relocate(clickEvent.getX(), 0); - guideLine.endYProperty().bind(heightProperty().subtract(dateAxis.heightProperty().subtract(dateAxis.tickLengthProperty()))); - - getChartChildren().add(guideLine); - - guideLine.setOnMouseClicked((MouseEvent event) -> { - if (event.getButton() == MouseButton.SECONDARY) { - clearGuideLine(); - event.consume(); - } - }); - } else { - guideLine.relocate(clickEvent.getX(), 0); - } - }); - } - - }, new ActionGroup( - NbBundle.getMessage(this.getClass(), "EventDetailChart.contextMenu.zoomHistory.name"), - new Back(controller), - new Forward(controller)))); - chartContextMenu.setAutoHide(true); + getChartContextMenu(clickEvent); chartContextMenu.show(EventDetailChart.this, clickEvent.getScreenX(), clickEvent.getScreenY()); clickEvent.consume(); } - }; - - setOnMouseClicked(clickHandler); - + }); //use one handler with an if chain because it maintains state final ChartDragHandler dragHandler = new ChartDragHandler<>(this, getXAxis()); setOnMousePressed(dragHandler); setOnMouseReleased(dragHandler); setOnMouseDragged(dragHandler); - projectionMap.addListener((MapChangeListener.Change change) -> { - final Line valueRemoved = change.getValueRemoved(); - if (valueRemoved != null) { - getChartChildren().removeAll(valueRemoved); - } - final Line valueAdded = change.getValueAdded(); - if (valueAdded != null) { - getChartChildren().add(valueAdded); - } - }); - this.selectedNodes = selectedNodes; - this.selectedNodes.addListener(( - ListChangeListener.Change c) -> { - while (c.next()) { - c.getRemoved().forEach((AggregateEventNode t) -> { - projectionMap.remove(t); - }); - c.getAddedSubList().forEach((AggregateEventNode t) -> { - Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(t.getEvent().getSpan().getStartMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET, - dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(t.getEvent().getSpan().getEndMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET - ); - line.setStroke(t.getEvent().getType().getColor().deriveColor(0, 1, 1, .5)); - line.setStrokeWidth(PROJECTED_LINE_STROKE_WIDTH); - line.setStrokeLineCap(StrokeLineCap.ROUND); - projectionMap.put(t, line); - }); + this.selectedNodes.addListener(new SelectionChangeHandler()); + } - } + ObservableList> getEventBundles() { + return bundles; + } - this.controller.selectEventIDs(selectedNodes.stream() - .flatMap((AggregateEventNode aggNode) -> aggNode.getEvent().getEventIDs().stream()) - .collect(Collectors.toList())); - }); + TimeLineController getController() { + return controller; + } - requestChartLayout(); + @NbBundle.Messages({"EventDetailChart.chartContextMenu.placeMarker.name=Place Marker", + "EventDetailChart.contextMenu.zoomHistory.name=Zoom History"}) + ContextMenu getChartContextMenu(MouseEvent clickEvent) throws MissingResourceException { + if (chartContextMenu != null) { + chartContextMenu.hide(); + } + + chartContextMenu = ActionUtils.createContextMenu(Arrays.asList(new PlaceMarkerAction(clickEvent), + new ActionGroup(Bundle.EventDetailChart_contextMenu_zoomHistory_name(), + new Back(controller), + new Forward(controller)))); + chartContextMenu.setAutoHide(true); + return chartContextMenu; } @Override @@ -329,7 +261,7 @@ public final class EventDetailChart extends XYChart im intervalSelector = null; } - public synchronized SimpleBooleanProperty getBandByType() { + public synchronized SimpleBooleanProperty bandByTypeProperty() { return bandByType; } @@ -337,6 +269,7 @@ public final class EventDetailChart extends XYChart im public synchronized void setController(TimeLineController controller) { this.controller = controller; setModel(this.controller.getEventsModel()); + getController().getQuickHideFilters().addListener(layoutInvalidationListener); } @Override @@ -389,162 +322,133 @@ public final class EventDetailChart extends XYChart im getChartChildren().add(getIntervalSelector()); } - public synchronized SimpleBooleanProperty getOneEventPerRow() { + SimpleBooleanProperty oneEventPerRowProperty() { return oneEventPerRow; } - public synchronized SimpleBooleanProperty getTruncateAll() { + SimpleDoubleProperty getTruncateWidth() { + return truncateWidth; + } + + SimpleBooleanProperty truncateAllProperty() { return truncateAll; } - synchronized void setEventOnePerRow(Boolean t1) { - oneEventPerRow.set(t1); - } - - synchronized void setTruncateAll(Boolean t1) { - truncateAll.set(t1); - + SimpleObjectProperty< DescriptionVisibility> descrVisibilityProperty() { + return descrVisibility; } @Override - protected synchronized void dataItemAdded(Series series, int i, Data data) { - final AggregateEvent aggEvent = data.getYValue(); - AggregateEventNode eventNode = nodeMap.get(aggEvent); - if (eventNode == null) { - eventNode = new AggregateEventNode(aggEvent, null, this); - - eventNode.setLayoutX(getXAxis().getDisplayPosition(new DateTime(aggEvent.getSpan().getStartMillis()))); - data.setNode(eventNode); - nodeMap.put(aggEvent, eventNode); - nodeGroup.getChildren().add(eventNode); - requiresLayout = true; - } + protected synchronized void dataItemAdded(Series series, int i, Data data) { + final EventCluster eventCluster = data.getYValue(); + bundles.add(eventCluster); + EventStripe eventStripe = stripeDescMap.merge(ImmutablePair.of(eventCluster.getEventType(), eventCluster.getDescription()), + new EventStripe(eventCluster, null), + (EventStripe u, EventStripe v) -> { + EventStripeNode remove = stripeNodeMap.remove(u); + nodeGroup.getChildren().remove(remove); + remove = stripeNodeMap.remove(v); + nodeGroup.getChildren().remove(remove); + return EventStripe.merge(u, v); + } + ); + EventStripeNode stripeNode = new EventStripeNode(EventDetailChart.this, eventStripe, null); + stripeNodeMap.put(eventStripe, stripeNode); + nodeGroup.getChildren().add(stripeNode); + data.setNode(stripeNode); + layoutPlotChildren(); } @Override - protected synchronized void dataItemChanged(Data data) { + protected synchronized void dataItemChanged(Data data) { //TODO: can we use this to help with local detail level adjustment -jm throw new UnsupportedOperationException("Not supported yet."); // NON-NLS //To change body of generated methods, choose Tools | Templates. } @Override - protected synchronized void dataItemRemoved(Data data, Series series) { - nodeMap.remove(data.getYValue()); - nodeGroup.getChildren().remove(data.getNode()); + protected synchronized void dataItemRemoved(Data data, Series series) { + EventCluster eventCluster = data.getYValue(); + bundles.removeAll(eventCluster); + EventStripe removedStripe = stripeDescMap.remove(ImmutablePair.of(eventCluster.getEventType(), eventCluster.getDescription())); + EventStripeNode removedNode = stripeNodeMap.remove(removedStripe); + nodeGroup.getChildren().remove(removedNode); data.setNode(null); + layoutPlotChildren(); } - @Override - protected void layoutChildren() { - super.layoutChildren(); - - } - - /** - * Layout the nodes representing events via the following algorithm. - * - * we start with a list of nodes (each representing an event) - sort the - * list of nodes by span start time of the underlying event - initialize - * empty map (maxXatY) from y-position to max used x-value - for each node: - * -- autosize the node (based on text label) -- get the event's start and - * end positions from the dateaxis -- size the capsule representing event - * duration -- starting from the top of the chart: --- (1)check if maxXatY - * is to the left of the start position: -------if maxXatY less than start - * position , good, put the current node here, mark end position as maxXatY, - * go to next node -------if maxXatY greater than start position, increment - * y position, do -------------check(1) again until maxXatY less than start - * position - */ @Override protected synchronized void layoutPlotChildren() { + setCursor(Cursor.WAIT); + maxY.set(0); + if (bandByType.get()) { + stripeNodeMap.values().stream() + .collect(Collectors.groupingBy(EventStripeNode::getEventType)).values() + .forEach(inputNodes -> { + List stripeNodes = inputNodes.stream() + .sorted(Comparator.comparing(EventStripeNode::getStartMillis)) + .collect(Collectors.toList()); - if (requiresLayout) { - setCursor(Cursor.WAIT); - double minY = 0; - - maxY.set(0.0); - - if (bandByType.get() == false) { - - ObservableList nodes = FXCollections.observableArrayList(nodeMap.values()); - FXCollections.sort(nodes, new StartTimeComparator()); - layoutNodes(nodes, minY, 0); -// layoutNodes(new ArrayList<>(nodeMap.values()), minY, 0); - } else { - for (Series s : sortedSeriesList) { - ObservableList nodes = FXCollections.observableArrayList(Collections2.transform(s.getData(), Data::getNode)); - - FXCollections.sort(nodes, new StartTimeComparator()); - layoutNodes(nodes.filtered((Node n) -> n != null), minY, 0); - minY = maxY.get(); - } - } - setCursor(null); - requiresLayout = false; + maxY.set(layoutEventBundleNodes(stripeNodes, maxY.get())); + }); + } else { + List stripeNodes = stripeNodeMap.values().stream() + .sorted(Comparator.comparing(EventStripeNode::getStartMillis)) + .collect(Collectors.toList()); + maxY.set(layoutEventBundleNodes(stripeNodes, 0)); } layoutProjectionMap(); + setCursor(null); } @Override - protected synchronized void seriesAdded(Series series, int i) { + protected synchronized void seriesAdded(Series series, int i) { for (int j = 0; j < series.getData().size(); j++) { dataItemAdded(series, j, series.getData().get(j)); } seriesList.add(series); - requiresLayout = true; } @Override - protected synchronized void seriesRemoved(Series series) { + protected synchronized void seriesRemoved(Series series) { for (int j = 0; j < series.getData().size(); j++) { dataItemRemoved(series.getData().get(j), series); } seriesList.remove(series); - requiresLayout = true; } - synchronized SimpleObjectProperty getDescrVisibility() { - return descrVisibility; - } - - synchronized ReadOnlyDoubleProperty getMaxVScroll() { + ReadOnlyDoubleProperty maxVScrollProperty() { return maxY.getReadOnlyProperty(); } - Iterable getNodes(Predicate p) { - List nodes = new ArrayList<>(); + /** + * @return all the nodes that pass the given predicate + */ + Iterable> getNodes(Predicate> p) { + //use this recursive function to flatten the tree of nodes into an iterable. + Function, Stream>> stripeFlattener = + new Function, Stream>>() { + @Override + public Stream> apply(EventBundleNodeBase node) { + return Stream.concat( + Stream.of(node), + node.getSubNodes().stream().flatMap(this::apply)); + } + }; - for (AggregateEventNode node : nodeMap.values()) { - checkNode(node, p, nodes); - } - - return nodes; + return stripeNodeMap.values().stream() + .flatMap(stripeFlattener) + .filter(p).collect(Collectors.toList()); } - Iterable getAllNodes() { + Iterable> getAllNodes() { return getNodes(x -> true); } - synchronized SimpleDoubleProperty getTruncateWidth() { - return truncateWidth; - } - synchronized void setVScroll(double d) { final double h = maxY.get() - (getHeight() * .9); nodeGroup.setTranslateY(-d * h); } - private static void checkNode(AggregateEventNode node, Predicate p, List nodes) { - if (node != null) { - if (p.test(node)) { - nodes.add(node); - } - for (Node n : node.getSubNodePane().getChildrenUnmodifiable()) { - checkNode((AggregateEventNode) n, p, nodes); - } - } - } - private void clearGuideLine() { getChartChildren().remove(guideLine); guideLine = null; @@ -554,114 +458,123 @@ public final class EventDetailChart extends XYChart im * layout the nodes in the given list, starting form the given minimum y * coordinate. * - * @param nodes - * @param minY + * Layout the nodes representing events via the following algorithm. + * + * we start with a list of nodes (each representing an event) - sort the + * list of nodes by span start time of the underlying event - initialize + * empty map (maxXatY) from y-position to max used x-value - for each node: + * + * -- size the node based on its children (recursively) + * + * -- get the event's start position from the dateaxis + * + * -- to position node (1)check if maxXatY is to the left of the left x + * coord: if maxXatY is less than the left x coord, good, put the current + * node here, mark right x coord as maxXatY, go to next node ; if maxXatY + * greater than start position, increment y position, do check(1) again + * until maxXatY less than start position + * + * @param nodes collection of nodes to layout + * @param minY the minimum y coordinate to position the nodes at. */ - private synchronized double layoutNodes(final List nodes, final double minY, final double xOffset) { - //hash map from y value to right most occupied x value. This tells you for a given 'row' what is the first avaialable slot - Map maxXatY = new HashMap<>(); + synchronized double layoutEventBundleNodes(final Collection> nodes, final double minY) { + // map from y value (ranges) to right most occupied x value. + TreeRangeMap treeRangeMap = TreeRangeMap.create(); + // maximum y values occupied by any of the given nodes, updated as nodes are layed out. double localMax = minY; - //for each node lay size it and position it in first available slot - for (Node n : nodes) { - final AggregateEventNode tlNode = (AggregateEventNode) n; - tlNode.setDescriptionVisibility(descrVisibility.get()); - AggregateEvent ie = tlNode.getEvent(); - final double rawDisplayPosition = getXAxis().getDisplayPosition(new DateTime(ie.getSpan().getStartMillis())); - //position of start and end according to range of axis - double xPos = rawDisplayPosition - xOffset; - double layoutNodesResultHeight = 0; - if (tlNode.getSubNodePane().getChildren().isEmpty() == false) { - FXCollections.sort(tlNode.getSubNodePane().getChildren(), new StartTimeComparator()); - layoutNodesResultHeight = layoutNodes(tlNode.getSubNodePane().getChildren(), 0, rawDisplayPosition); - } - double xPos2 = getXAxis().getDisplayPosition(new DateTime(ie.getSpan().getEndMillis())) - xOffset; - double span = xPos2 - xPos; + //for each node do a recursive layout to size it and then position it in first available slot + for (final EventBundleNodeBase bundleNode : nodes) { + //is the node hiden by a quick hide filter? + boolean quickHide = getController().getQuickHideFilters().stream() + .filter(AbstractFilter::isActive) + .anyMatch(filter -> filter.getDescription().equals(bundleNode.getDescription())); + if (quickHide) { + //hide it and skip layout + bundleNode.setVisible(false); + bundleNode.setManaged(false); + } else { + //make sure it is shown + bundleNode.setVisible(true); + bundleNode.setManaged(true); + //apply advanced layout description visibility options + bundleNode.setDescriptionVisibilityLevel(descrVisibility.get()); + bundleNode.setDescriptionWidth(truncateAll.get() ? truncateWidth.get() : USE_PREF_SIZE); - //size timespan border - tlNode.setSpanWidth(span); - if (truncateAll.get()) { //if truncate option is selected limit width of description label - tlNode.setDescriptionWidth(Math.max(span, truncateWidth.get())); - } else { //else set it unbounded - tlNode.setDescriptionWidth(USE_PREF_SIZE);//20 + new Text(tlNode.getDisplayedDescription()).getLayoutBounds().getWidth()); - } - tlNode.autosize(); //compute size of tlNode based on constraints and event data + //do recursive layout + bundleNode.layout(); + //get computed height and width + double h = bundleNode.getBoundsInLocal().getHeight(); + double w = bundleNode.getBoundsInLocal().getWidth(); + //get left and right x coords from axis plus computed width + double xLeft = getXForEpochMillis(bundleNode.getStartMillis()) - bundleNode.getLayoutXCompensation(); + double xRight = xLeft + w; - //get position of right edge of node ( influenced by description label) - double xRight = xPos + tlNode.getWidth(); + //initial test position + double yTop = minY; + double yBottom = yTop + h; - //get the height of the node - final double h = layoutNodesResultHeight == 0 ? tlNode.getHeight() : layoutNodesResultHeight + DEFAULT_ROW_HEIGHT; - //initial test position - double yPos = minY; - - double yPos2 = yPos + h; - - if (oneEventPerRow.get()) { - // if onePerRow, just put it at end - yPos = (localMax + 2); - yPos2 = yPos + h; - - } else {//else - - boolean overlapping = true; - while (overlapping) { - //loop through y values looking for available slot. - - overlapping = false; - //check each pixel from bottom to top. - for (double y = yPos2; y >= yPos; y--) { - final Double maxX = maxXatY.get((int) y); - if (maxX != null && maxX >= xPos - 4) { - //if that pixel is already used - //jump top to this y value and repeat until free slot is found. - overlapping = true; - yPos = y + 4; - yPos2 = yPos + h; - break; + if (oneEventPerRow.get()) { + // if onePerRow, just put it at end + yTop = (localMax + MINIMUM_EVENT_NODE_GAP); + yBottom = yTop + h; + } else { + //until the node is not overlapping any others try moving it down. + boolean overlapping = true; + while (overlapping) { + overlapping = false; + //check each pixel from bottom to top. + for (double y = yBottom; y >= yTop; y--) { + final Double maxX = treeRangeMap.get(y); + if (maxX != null && maxX >= xLeft - MINIMUM_EVENT_NODE_GAP) { + //if that pixel is already used + //jump top to this y value and repeat until free slot is found. + overlapping = true; + yTop = y + MINIMUM_EVENT_NODE_GAP; + yBottom = yTop + h; + break; + } } } + treeRangeMap.put(Range.closed(yTop, yBottom), xRight); } - //mark used y values - for (double y = yPos; y <= yPos2; y++) { - maxXatY.put((int) y, xRight); - } + + localMax = Math.max(yBottom, localMax); + + //animate node to new position + Timeline timeline = new Timeline(new KeyFrame(Duration.millis(100), + new KeyValue(bundleNode.layoutXProperty(), xLeft), + new KeyValue(bundleNode.layoutYProperty(), yTop))); + timeline.setOnFinished((ActionEvent event) -> { + requestChartLayout(); + }); + timeline.play(); } - localMax = Math.max(yPos2, localMax); - - Timeline tm = new Timeline(new KeyFrame(Duration.seconds(1.0), - new KeyValue(tlNode.layoutXProperty(), xPos), - new KeyValue(tlNode.layoutYProperty(), yPos))); - - tm.play(); -// tlNode.relocate(xPos, yPos); } - maxY.set(Math.max(maxY.get(), localMax)); - return localMax - minY; + return localMax; //return new max + } + + private double getXForEpochMillis(Long millis) { + DateTime dateTime = new DateTime(millis, TimeLineController.getJodaTimeZone()); + return getXAxis().getDisplayPosition(new DateTime(dateTime)); } - private static final int DEFAULT_ROW_HEIGHT = 24; private void layoutProjectionMap() { - for (final Map.Entry entry : projectionMap.entrySet()) { - final AggregateEventNode aggNode = entry.getKey(); + for (final Map.Entry entry : projectionMap.entrySet()) { + final EventCluster cluster = entry.getKey(); final Line line = entry.getValue(); - line.setStartX(getParentXForValue(new DateTime(aggNode.getEvent().getSpan().getStartMillis(), TimeLineController.getJodaTimeZone()))); - line.setEndX(getParentXForValue(new DateTime(aggNode.getEvent().getSpan().getEndMillis(), TimeLineController.getJodaTimeZone()))); + line.setStartX(getParentXForEpochMillis(cluster.getStartMillis())); + line.setEndX(getParentXForEpochMillis(cluster.getEndMillis())); + line.setStartY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET); line.setEndY(getXAxis().getLayoutY() + PROJECTED_LINE_Y_OFFSET); } } - private double getParentXForValue(DateTime dt) { - return getXAxis().localToParent(getXAxis().getDisplayPosition(dt), 0).getX(); - } - - /** - * @return the controller - */ - public TimeLineController getController() { - return controller; + private double getParentXForEpochMillis(Long epochMillis) { + DateTime dateTime = new DateTime(epochMillis, TimeLineController.getJodaTimeZone()); + return getXAxis().localToParent(getXAxis().getDisplayPosition(dateTime), 0).getX(); } /** @@ -671,34 +584,9 @@ public final class EventDetailChart extends XYChart im return filteredEvents; } - /** - * @return the chartContextMenu - */ - public ContextMenu getChartContextMenu() { - return chartContextMenu; - } + static private class DetailIntervalSelector extends IntervalSelector { - private static class StartTimeComparator implements Comparator { - - @Override - public int compare(Node n1, Node n2) { - - if (n1 == null) { - return 1; - } else if (n2 == null) { - return -1; - } else { - - return Long.compare(((AggregateEventNode) n1).getEvent().getSpan().getStartMillis(), - (((AggregateEventNode) n2).getEvent().getSpan().getStartMillis())); - } - } - - } - - private class DetailIntervalSelector extends IntervalSelector { - - public DetailIntervalSelector(double x, double height, Axis axis, TimeLineController controller) { + DetailIntervalSelector(double x, double height, Axis axis, TimeLineController controller) { super(x, height, axis, controller); } @@ -716,18 +604,108 @@ public final class EventDetailChart extends XYChart im protected DateTime parseDateTime(DateTime date) { return date; } - } - synchronized void setRequiresLayout(boolean b) { - requiresLayout = true; + private class PlaceMarkerAction extends Action { + + PlaceMarkerAction(MouseEvent clickEvent) { + super(Bundle.EventDetailChart_chartContextMenu_placeMarker_name()); + + setGraphic(new ImageView(MARKER)); // NON-NLS + setEventHandler(actionEvent -> { + if (guideLine == null) { + guideLine = new GuideLine(0, 0, 0, getHeight(), getXAxis()); + guideLine.relocate(sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0); + guideLine.endYProperty().bind(heightProperty().subtract(getXAxis().heightProperty().subtract(getXAxis().tickLengthProperty()))); + getChartChildren().add(guideLine); + guideLine.setOnMouseClicked(mouseEvent -> { + if (mouseEvent.getButton() == MouseButton.SECONDARY) { + clearGuideLine(); + mouseEvent.consume(); + } + }); + } else { + guideLine.relocate(sceneToLocal(clickEvent.getSceneX(), 0).getX(), 0); + } + }); + } } - /** - * make this accessible to AggregateEventNode - */ - @Override - protected void requestChartLayout() { - super.requestChartLayout(); + private class SelectionChangeHandler implements ListChangeListener> { + + private final Axis dateAxis; + + SelectionChangeHandler() { + dateAxis = getXAxis(); + } + + @Override + public void onChanged(ListChangeListener.Change> change) { + while (change.next()) { + change.getRemoved().forEach((EventBundleNodeBase removedNode) -> { + removedNode.getEventBundle().getClusters().forEach(cluster -> { + Line removedLine = projectionMap.remove(cluster); + getChartChildren().removeAll(removedLine); + }); + + }); + change.getAddedSubList().forEach((EventBundleNodeBase addedNode) -> { + + for (EventCluster range : addedNode.getEventBundle().getClusters()) { + + Line line = new Line(dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(range.getStartMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET, + dateAxis.localToParent(dateAxis.getDisplayPosition(new DateTime(range.getEndMillis(), TimeLineController.getJodaTimeZone())), 0).getX(), dateAxis.getLayoutY() + PROJECTED_LINE_Y_OFFSET + ); + line.setStroke(addedNode.getEventType().getColor().deriveColor(0, 1, 1, .5)); + line.setStrokeWidth(PROJECTED_LINE_STROKE_WIDTH); + line.setStrokeLineCap(StrokeLineCap.ROUND); + projectionMap.put(range, line); + getChartChildren().add(line); + } + }); + } + EventDetailChart.this.controller.selectEventIDs(selectedNodes.stream() + .flatMap(detailNode -> detailNode.getEventIDs().stream()) + .collect(Collectors.toList())); + } + } + + class HideDescriptionAction extends Action { + + HideDescriptionAction(String description, DescriptionLoD descriptionLoD) { + super("Hide"); + setGraphic(new ImageView(HIDE)); + setEventHandler((ActionEvent t) -> { + final DescriptionFilter testFilter = new DescriptionFilter( + descriptionLoD, + description, + DescriptionFilter.FilterMode.EXCLUDE); + + DescriptionFilter descriptionFilter = getController().getQuickHideFilters().stream() + .filter(testFilter::equals) + .findFirst().orElseGet(() -> { + testFilter.selectedProperty().addListener((Observable observable) -> { + layoutPlotChildren(); + }); + getController().getQuickHideFilters().add(testFilter); + return testFilter; + }); + descriptionFilter.setSelected(true); + }); + } + } + + class UnhideDescriptionAction extends Action { + + UnhideDescriptionAction(String description, DescriptionLoD descriptionLoD) { + super("Unhide"); + setGraphic(new ImageView(SHOW)); + setEventHandler((ActionEvent t) -> + getController().getQuickHideFilters().stream() + .filter(descriptionFilter -> descriptionFilter.getDescriptionLoD().equals(descriptionLoD) + && descriptionFilter.getDescription().equals(description)) + .forEach(descriptionfilter -> descriptionfilter.setSelected(false)) + ); + } } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java new file mode 100644 index 0000000000..e840b278de --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/EventStripeNode.java @@ -0,0 +1,181 @@ +/* + + * Autopsy Forensic Browser + * + * Copyright 2015 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.timeline.ui.detailview; + +import javafx.event.EventHandler; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.MenuItem; +import javafx.scene.control.OverrunStyle; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.VBox; +import org.apache.commons.lang3.StringUtils; +import org.controlsfx.control.action.ActionUtils; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.timeline.datamodel.EventCluster; +import org.sleuthkit.autopsy.timeline.datamodel.EventStripe; +import static org.sleuthkit.autopsy.timeline.ui.detailview.EventBundleNodeBase.configureLoDButton; + +/** + * Node used in {@link EventDetailChart} to represent an EventStripe. + */ +final public class EventStripeNode extends EventBundleNodeBase { + + private static final Logger LOGGER = Logger.getLogger(EventStripeNode.class.getName()); + final Button hideButton; + /** + * Pane that contains EventStripeNodes for any 'subevents' if they are + * displayed + * + * //TODO: move more of the control of subnodes/events here and out of + * EventDetail Chart + */ +// private final HBox clustersHBox = new HBox(); + private final ImageView eventTypeImageView = new ImageView(); + + public EventStripeNode(EventDetailChart chart, EventStripe eventStripe, EventClusterNode parentNode) { + super(chart, eventStripe, parentNode); + + setMinHeight(48); + + EventDetailChart.HideDescriptionAction hideClusterAction = chart.new HideDescriptionAction(getDescription(), eventBundle.getDescriptionLoD()); + hideButton = ActionUtils.createButton(hideClusterAction, ActionUtils.ActionTextBehavior.HIDE); + configureLoDButton(hideButton); + + infoHBox.getChildren().add(hideButton); + //setup description label + eventTypeImageView.setImage(getEventType().getFXImage()); + descrLabel.setPrefWidth(USE_COMPUTED_SIZE); + descrLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); + descrLabel.setGraphic(eventTypeImageView); + + setAlignment(subNodePane, Pos.BOTTOM_LEFT); + for (EventCluster cluster : eventStripe.getClusters()) { + EventClusterNode clusterNode = new EventClusterNode(chart, cluster, this); + subNodes.add(clusterNode); + subNodePane.getChildren().addAll(clusterNode); + } + + getChildren().addAll(new VBox(infoHBox, subNodePane)); + setOnMouseClicked(new MouseClickHandler()); + } + + @Override + void showHoverControls(final boolean showControls) { + super.showHoverControls(showControls); + show(hideButton, showControls); + } + + public EventStripe getEventStripe() { + return getEventBundle(); + } + + /** + * @param w the maximum width the description label should have + */ + @Override + public void setDescriptionWidth(double w) { + descrLabel.setMaxWidth(w); + } + + /** + * apply the 'effect' to visually indicate highlighted nodes + * + * @param applied true to apply the highlight 'effect', false to remove it + */ + @Override + public synchronized void applyHighlightEffect(boolean applied) { + if (applied) { + descrLabel.setStyle("-fx-font-weight: bold;"); // NON-NLS + setBackground(highlightedBackground); + } else { + descrLabel.setStyle("-fx-font-weight: normal;"); // NON-NLS + setBackground(defaultBackground); + } + } + + @Override + public void setDescriptionVisibility(DescriptionVisibility descrVis) { + final int size = getEventStripe().getEventIDs().size(); + + switch (descrVis) { + case HIDDEN: + countLabel.setText(""); + descrLabel.setText(""); + break; + case COUNT_ONLY: + descrLabel.setText(""); + countLabel.setText(String.valueOf(size)); + break; + default: + case SHOWN: + String description = getEventStripe().getDescription(); + description = parentNode != null + ? " ..." + StringUtils.substringAfter(description, parentNode.getDescription()) + : description; + descrLabel.setText(description); + countLabel.setText(((size == 1) ? "" : " (" + size + ")")); // NON-NLS + break; + } + } + + /** + * event handler used for mouse events on {@link EventStripeNode}s + */ + private class MouseClickHandler implements EventHandler { + + private ContextMenu contextMenu; + + @Override + public void handle(MouseEvent t) { + + if (t.getButton() == MouseButton.PRIMARY) { + + if (t.isShiftDown()) { + if (chart.selectedNodes.contains(EventStripeNode.this) == false) { + chart.selectedNodes.add(EventStripeNode.this); + } + } else if (t.isShortcutDown()) { + chart.selectedNodes.removeAll(EventStripeNode.this); + } else { + chart.selectedNodes.setAll(EventStripeNode.this); + } + t.consume(); + } else if (t.getButton() == MouseButton.SECONDARY) { + ContextMenu chartContextMenu = chart.getChartContextMenu(t); + if (contextMenu == null) { + contextMenu = new ContextMenu(); + contextMenu.setAutoHide(true); + + EventDetailChart.HideDescriptionAction hideClusterAction = chart.new HideDescriptionAction(getDescription(), eventBundle.getDescriptionLoD()); + MenuItem hideDescriptionMenuItem = ActionUtils.createMenuItem(hideClusterAction); + contextMenu.getItems().addAll(hideDescriptionMenuItem); + contextMenu.getItems().addAll(chartContextMenu.getItems()); + } + contextMenu.show(EventStripeNode.this, t.getScreenX(), t.getScreenY()); + t.consume(); + } + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java index ab8b65cd00..3a2dad163b 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/GuideLine.java @@ -19,6 +19,7 @@ package org.sleuthkit.autopsy.timeline.ui.detailview; import javafx.scene.Cursor; +import javafx.scene.chart.Axis; import javafx.scene.control.Tooltip; import javafx.scene.input.MouseEvent; import javafx.scene.paint.Color; @@ -32,7 +33,7 @@ import org.sleuthkit.autopsy.timeline.TimeLineController; */ class GuideLine extends Line { - private final DateAxis dateAxis; + private final Axis dateAxis; private double startLayoutX; @@ -40,7 +41,7 @@ class GuideLine extends Line { private double dragStartX = 0; - GuideLine(double startX, double startY, double endX, double endY, DateAxis axis) { + GuideLine(double startX, double startY, double endX, double endY, Axis axis) { super(startX, startY, endX, endY); dateAxis = axis; setCursor(Cursor.E_RESIZE); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java index 04ec644a25..6feef28f8f 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventDescriptionTreeItem.java @@ -19,42 +19,72 @@ package org.sleuthkit.autopsy.timeline.ui.detailview.tree; import java.util.Comparator; +import java.util.Deque; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import javafx.collections.FXCollections; import javafx.scene.control.TreeItem; -import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent; +import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; /** * */ class EventDescriptionTreeItem extends NavTreeItem { - public EventDescriptionTreeItem(AggregateEvent g) { - setValue(new NavTreeNode(g.getType().getBaseType(), g.getDescription(), g.getEventIDs().size())); + /** + * maps a description to the child item of this item with that description + */ + private final Map childMap = new ConcurrentHashMap<>(); + private final EventBundle bundle; + + public EventBundle getEventBundle() { + return bundle; + } + + EventDescriptionTreeItem(EventBundle g) { + bundle = g; + setValue(g); } @Override - public int getCount() { + public long getCount() { return getValue().getCount(); } - @Override - public void insert(AggregateEvent g) { - NavTreeNode value = getValue(); - if ((value.getType().getBaseType().equals(g.getType().getBaseType()) == false) || ((value.getDescription().equals(g.getDescription()) == false))) { - throw new IllegalArgumentException(); + public void insert(Deque> path) { + EventBundle head = path.removeFirst(); + EventDescriptionTreeItem treeItem = childMap.get(head.getDescription()); + if (treeItem == null) { + treeItem = new EventDescriptionTreeItem(head); + treeItem.setExpanded(true); + childMap.put(head.getDescription(), treeItem); + getChildren().add(treeItem); + FXCollections.sort(getChildren(), TreeComparator.Description); } - setValue(new NavTreeNode(value.getType().getBaseType(), value.getDescription(), value.getCount() + g.getEventIDs().size())); + if (path.isEmpty() == false) { + treeItem.insert(path); + } } @Override - public void resort(Comparator> comp) { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + public void resort(Comparator>> comp) { + FXCollections.sort(getChildren(), comp); } @Override - public TreeItem findTreeItemForEvent(AggregateEvent t) { - if (getValue().getType().getBaseType() == t.getType().getBaseType() && getValue().getDescription().equals(t.getDescription())) { + public NavTreeItem findTreeItemForEvent(EventBundle t) { + + if (getValue().getEventType() == t.getEventType() + && getValue().getDescription().equals(t.getDescription())) { return this; + } else { + for (EventDescriptionTreeItem child : childMap.values()) { + final NavTreeItem findTreeItemForEvent = child.findTreeItemForEvent(t); + if (findTreeItemForEvent != null) { + return findTreeItemForEvent; + } + } } return null; } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java index ac16b31949..e921991191 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/EventTypeTreeItem.java @@ -19,12 +19,12 @@ package org.sleuthkit.autopsy.timeline.ui.detailview.tree; import java.util.Comparator; +import java.util.Deque; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.scene.control.TreeItem; -import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent; +import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; class EventTypeTreeItem extends NavTreeItem { @@ -33,55 +33,39 @@ class EventTypeTreeItem extends NavTreeItem { */ private final Map childMap = new ConcurrentHashMap<>(); - private final Comparator> comparator = TreeComparator.Description; + private final Comparator>> comparator = TreeComparator.Description; - EventTypeTreeItem(AggregateEvent g) { - setValue(new NavTreeNode(g.getType().getBaseType(), g.getType().getBaseType().getDisplayName(), 0)); + EventTypeTreeItem(EventBundle g) { + setValue(g); } @Override - public int getCount() { + public long getCount() { return getValue().getCount(); } - /** - * Recursive method to add a grouping at a given path. - * - * @param path Full path (or subset not yet added) to add - * @param g Group to add - * @param tree True if it is part of a tree (versus a list) - */ - @Override - public void insert(AggregateEvent g) { - - EventDescriptionTreeItem treeItem = childMap.get(g.getDescription()); + public void insert(Deque> path) { + EventBundle head = path.removeFirst(); + EventDescriptionTreeItem treeItem = childMap.get(head.getDescription()); if (treeItem == null) { - final EventDescriptionTreeItem newTreeItem = new EventDescriptionTreeItem(g); - newTreeItem.setExpanded(true); - childMap.put(g.getDescription(), newTreeItem); - - Platform.runLater(() -> { - synchronized (getChildren()) { - getChildren().add(newTreeItem); - FXCollections.sort(getChildren(), comparator); - } - }); - } else { - treeItem.insert(g); + treeItem = new EventDescriptionTreeItem(head); + treeItem.setExpanded(true); + childMap.put(head.getDescription(), treeItem); + getChildren().add(treeItem); + FXCollections.sort(getChildren(), comparator); } - Platform.runLater(() -> { - NavTreeNode value1 = getValue(); - setValue(new NavTreeNode(value1.getType().getBaseType(), value1.getType().getBaseType().getDisplayName(), childMap.values().stream().mapToInt(EventDescriptionTreeItem::getCount).sum())); - }); + if (path.isEmpty() == false) { + treeItem.insert(path); + } } @Override - public TreeItem findTreeItemForEvent(AggregateEvent t) { - if (t.getType().getBaseType() == getValue().getType().getBaseType()) { + public NavTreeItem findTreeItemForEvent(EventBundle t) { + if (t.getEventType().getBaseType() == getValue().getEventType().getBaseType()) { - for (TreeItem child : getChildren()) { - final TreeItem findTreeItemForEvent = ((NavTreeItem) child).findTreeItemForEvent(t); + for (EventDescriptionTreeItem child : childMap.values()) { + final NavTreeItem findTreeItemForEvent = child.findTreeItemForEvent(t); if (findTreeItemForEvent != null) { return findTreeItemForEvent; } @@ -91,7 +75,7 @@ class EventTypeTreeItem extends NavTreeItem { } @Override - public void resort(Comparator> comp) { + public void resort(Comparator>> comp) { FXCollections.sort(getChildren(), comp); } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java index 361c8b3250..7b96a0ea53 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavPanel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013 Basis Technology Corp. + * Copyright 2013-15 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,15 +18,17 @@ */ package org.sleuthkit.autopsy.timeline.ui.detailview.tree; -import java.net.URL; +import com.google.common.collect.ImmutableList; import java.util.Arrays; +import java.util.Collection; import java.util.Comparator; -import java.util.ResourceBundle; -import javafx.application.Platform; +import java.util.Objects; +import javafx.beans.InvalidationListener; import javafx.beans.Observable; -import javafx.collections.ObservableList; +import javafx.collections.ListChangeListener; import javafx.fxml.FXML; import javafx.scene.control.ComboBox; +import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.SelectionMode; import javafx.scene.control.Tooltip; @@ -36,20 +38,26 @@ import javafx.scene.control.TreeView; import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; +import org.apache.commons.lang3.StringUtils; +import org.controlsfx.control.action.ActionUtils; import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.timeline.FXMLConstructor; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.TimeLineView; -import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent; +import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; -import org.sleuthkit.autopsy.timeline.ui.detailview.AggregateEventNode; +import org.sleuthkit.autopsy.timeline.filters.AbstractFilter; +import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter; import org.sleuthkit.autopsy.timeline.ui.detailview.DetailViewPane; /** - * Display two trees. one shows all folders (groups) and calls out folders with - * images. the user can select folders with images to see them in the main - * GroupListPane The other shows folders with hash set hits. + * Shows all {@link EventBundles} from the assigned {@link DetailViewPane} in a + * tree organized by type and then description. Hidden bundles are shown grayed + * out. Right clicking on a item in the tree shows a context menu to show/hide + * it. */ public class NavPanel extends BorderPane implements TimeLineView { @@ -57,59 +65,51 @@ public class NavPanel extends BorderPane implements TimeLineView { private FilteredEventsModel filteredEvents; - @FXML - private ResourceBundle resources; - - @FXML - private URL location; - private DetailViewPane detailViewPane; - /** - * TreeView for folders with hash hits - */ @FXML - private TreeView< NavTreeNode> eventsTree; + private TreeView> eventsTree; @FXML private Label eventsTreeLabel; @FXML - private ComboBox>> sortByBox; + private ComboBox>>> sortByBox; public NavPanel() { - - FXMLConstructor.construct(this, "NavPanel.fxml"); // NON-NLS + FXMLConstructor.construct(this, "NavPanel.fxml"); // NON-NLS } - public void setChart(DetailViewPane detailViewPane) { + public void setDetailViewPane(DetailViewPane detailViewPane) { this.detailViewPane = detailViewPane; detailViewPane.setSelectionModel(eventsTree.getSelectionModel()); - setRoot(); - detailViewPane.getAggregatedEvents().addListener((Observable observable) -> { + + detailViewPane.getEventBundles().addListener((Observable observable) -> { setRoot(); }); + setRoot(); + detailViewPane.getSelectedNodes().addListener((Observable observable) -> { eventsTree.getSelectionModel().clearSelection(); - detailViewPane.getSelectedNodes().forEach((AggregateEventNode t) -> { - eventsTree.getSelectionModel().select(((NavTreeItem) eventsTree.getRoot()).findTreeItemForEvent(t.getEvent())); + detailViewPane.getSelectedNodes().forEach(eventBundleNode -> { + eventsTree.getSelectionModel().select(getRoot().findTreeItemForEvent(eventBundleNode.getEventBundle())); }); }); } + private NavTreeItem getRoot() { + return (NavTreeItem) eventsTree.getRoot(); + } + + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) private void setRoot() { RootItem root = new RootItem(); - final ObservableList aggregatedEvents = detailViewPane.getAggregatedEvents(); - - synchronized (aggregatedEvents) { - for (AggregateEvent agg : aggregatedEvents) { - root.insert(agg); - } + for (EventBundle bundle : detailViewPane.getEventBundles()) { + root.insert(bundle); } - Platform.runLater(() -> { - eventsTree.setRoot(root); - }); + eventsTree.setRoot(root); + } @Override @@ -131,40 +131,107 @@ public class NavPanel extends BorderPane implements TimeLineView { sortByBox.getItems().setAll(Arrays.asList(TreeComparator.Description, TreeComparator.Count)); sortByBox.getSelectionModel().select(TreeComparator.Description); sortByBox.getSelectionModel().selectedItemProperty().addListener((Observable o) -> { - ((RootItem) eventsTree.getRoot()).resort(sortByBox.getSelectionModel().getSelectedItem()); + getRoot().resort(sortByBox.getSelectionModel().getSelectedItem()); }); eventsTree.setShowRoot(false); - eventsTree.setCellFactory((TreeView p) -> new EventTreeCell()); + eventsTree.setCellFactory((TreeView> p) -> new EventBundleTreeCell()); eventsTree.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); eventsTreeLabel.setText(NbBundle.getMessage(this.getClass(), "NavPanel.eventsTreeLabel.text")); } /** - * A tree cell to display {@link NavTreeNode}s. Shows the description, and + * A tree cell to display {@link EventBundle}s. Shows the description, and * count, as well a a "legend icon" for the event type. */ - private static class EventTreeCell extends TreeCell { + private class EventBundleTreeCell extends TreeCell> { + + private static final double HIDDEN_MULTIPLIER = .6; + private final Rectangle rect = new Rectangle(24, 24); + private final ImageView imageView = new ImageView(); + private InvalidationListener filterStateChangeListener; + + EventBundleTreeCell() { + rect.setArcHeight(5); + rect.setArcWidth(5); + rect.setStrokeWidth(2); + } @Override - protected void updateItem(NavTreeNode item, boolean empty) { + protected void updateItem(EventBundle item, boolean empty) { super.updateItem(item, empty); - if (item != null) { - final String text = item.getDescription() + " (" + item.getCount() + ")"; // NON-NLS - setText(text); - setTooltip(new Tooltip(text)); - Rectangle rect = new Rectangle(24, 24); - rect.setArcHeight(5); - rect.setArcWidth(5); - rect.setStrokeWidth(2); - rect.setStroke(item.getType().getColor()); - rect.setFill(item.getType().getColor().deriveColor(0, 1, 1, 0.1)); - setGraphic(new StackPane(rect, new ImageView(item.getType().getFXImage()))); - } else { + if (item == null || empty) { setText(null); setTooltip(null); setGraphic(null); + setContextMenu(null); + deRegisterListeners(controller.getQuickHideFilters()); + } else { + filterStateChangeListener = (filterState) -> updateHiddenState(item); + controller.getQuickHideFilters().addListener((ListChangeListener.Change listChange) -> { + while (listChange.next()) { + deRegisterListeners(listChange.getRemoved()); + registerListeners(listChange.getAddedSubList(), item); + } + updateHiddenState(item); + }); + registerListeners(controller.getQuickHideFilters(), item); + String text = item.getDescription() + " (" + item.getCount() + ")"; // NON-NLS + TreeItem> parent = getTreeItem().getParent(); + if (parent != null && parent.getValue() != null && (parent instanceof EventDescriptionTreeItem)) { + text = StringUtils.substringAfter(text, parent.getValue().getDescription()); + } + setText(text); + setTooltip(new Tooltip(text)); + imageView.setImage(item.getEventType().getFXImage()); + setGraphic(new StackPane(rect, imageView)); + updateHiddenState(item); } } + + private void registerListeners(Collection filters, EventBundle item) { + for (DescriptionFilter filter : filters) { + if (filter.getDescription().equals(item.getDescription())) { + filter.activeProperty().addListener(filterStateChangeListener); + } + } + } + + private void deRegisterListeners(Collection filters) { + if (Objects.nonNull(filterStateChangeListener)) { + for (DescriptionFilter filter : filters) { + filter.activeProperty().removeListener(filterStateChangeListener); + } + } + } + + private void updateHiddenState(EventBundle item) { + TreeItem> treeItem = getTreeItem(); + ContextMenu newMenu; + if (controller.getQuickHideFilters().stream(). + filter(AbstractFilter::isActive) + .anyMatch(filter -> filter.getDescription().equals(item.getDescription()))) { + if (treeItem != null) { + treeItem.setExpanded(false); + } + setTextFill(Color.gray(0, HIDDEN_MULTIPLIER)); + imageView.setOpacity(HIDDEN_MULTIPLIER); + rect.setStroke(item.getEventType().getColor().deriveColor(0, HIDDEN_MULTIPLIER, 1, HIDDEN_MULTIPLIER)); + rect.setFill(item.getEventType().getColor().deriveColor(0, HIDDEN_MULTIPLIER, HIDDEN_MULTIPLIER, 0.1)); + newMenu = ActionUtils.createContextMenu(ImmutableList.of(detailViewPane.newUnhideDescriptionAction(item.getDescription(), item.getDescriptionLoD()))); + } else { + setTextFill(Color.BLACK); + imageView.setOpacity(1); + rect.setStroke(item.getEventType().getColor()); + rect.setFill(item.getEventType().getColor().deriveColor(0, 1, 1, 0.1)); + newMenu = ActionUtils.createContextMenu(ImmutableList.of(detailViewPane.newHideDescriptionAction(item.getDescription(), item.getDescriptionLoD()))); + } + if (treeItem instanceof EventDescriptionTreeItem) { + setContextMenu(newMenu); + } else { + setContextMenu(null); + } + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java index 3f96c57a71..bed99d2270 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeItem.java @@ -20,22 +20,19 @@ package org.sleuthkit.autopsy.timeline.ui.detailview.tree; import java.util.Comparator; import javafx.scene.control.TreeItem; -import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent; +import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; /** * A node in the nav tree. Manages inserts and resorts. Has parents and * children. Does not have graphical properties these are configured in - * {@link EventTreeCell}. Each GroupTreeItem has a NavTreeNode which has a type, - * description , and count + * {@link EventTreeCell}. Each NavTreeItem has a EventBundle which has a type, + * description , count, etc. */ -abstract class NavTreeItem extends TreeItem { +abstract class NavTreeItem extends TreeItem> { - abstract void insert(AggregateEvent g); + abstract long getCount(); - abstract int getCount(); - - abstract void resort(Comparator> comp); - - abstract TreeItem findTreeItemForEvent(AggregateEvent t); + abstract void resort(Comparator>> comp); + abstract NavTreeItem findTreeItemForEvent(EventBundle t); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeNode.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeNode.java deleted file mode 100644 index 4d193da57b..0000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/NavTreeNode.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2014 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.timeline.ui.detailview.tree; - -import javax.annotation.concurrent.Immutable; -import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; - -/** - * The data item for the nav tree. Represents a combination of type and - * description, as well as the corresponding number of events - */ -@Immutable -public class NavTreeNode { - - final private EventType type; - - final private String Description; - - final private int count; - - public EventType getType() { - return type; - } - - public String getDescription() { - return Description; - } - - public int getCount() { - return count; - } - - public NavTreeNode(EventType type, String Description, int count) { - this.type = type; - this.Description = Description; - this.count = count; - } - -} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java index 95ec59287b..571758a037 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/RootItem.java @@ -18,14 +18,15 @@ */ package org.sleuthkit.autopsy.timeline.ui.detailview.tree; +import java.util.ArrayDeque; import java.util.Comparator; +import java.util.Deque; import java.util.HashMap; import java.util.Map; -import javafx.application.Platform; -import javafx.collections.FXCollections; +import java.util.Optional; import javafx.scene.control.TreeItem; +import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; -import org.sleuthkit.autopsy.timeline.datamodel.AggregateEvent; /** * @@ -46,7 +47,7 @@ class RootItem extends NavTreeItem { } @Override - public int getCount() { + public long getCount() { return getValue().getCount(); } @@ -55,39 +56,41 @@ class RootItem extends NavTreeItem { * * @param g Group to add */ - @Override - public void insert(AggregateEvent g) { + public void insert(EventBundle g) { - EventTypeTreeItem treeItem = childMap.get(g.getType().getBaseType()); - if (treeItem == null) { - final EventTypeTreeItem newTreeItem = new EventTypeTreeItem(g); - newTreeItem.setExpanded(true); - childMap.put(g.getType().getBaseType(), newTreeItem); - newTreeItem.insert(g); - - Platform.runLater(() -> { - synchronized (getChildren()) { + EventTypeTreeItem treeItem = childMap.computeIfAbsent(g.getEventType().getBaseType(), + baseType -> { + EventTypeTreeItem newTreeItem = new EventTypeTreeItem(g); + newTreeItem.setExpanded(true); getChildren().add(newTreeItem); + getChildren().sort(TreeComparator.Type); + return newTreeItem; + }); + treeItem.insert(getTreePath(g)); + } - FXCollections.sort(getChildren(), TreeComparator.Type); - } - }); - } else { - treeItem.insert(g); + static Deque< EventBundle> getTreePath(EventBundle g) { + Deque> path = new ArrayDeque<>(); + Optional> p = Optional.of(g); + + while (p.isPresent()) { + EventBundle parent = p.get(); + path.addFirst(parent); + p = parent.getParentBundle(); } + + return path; } @Override - public void resort(Comparator> comp) { - childMap.values().forEach((ti) -> { - ti.resort(comp); - }); + public void resort(Comparator>> comp) { + childMap.values().forEach(ti -> ti.resort(comp)); } @Override - public TreeItem findTreeItemForEvent(AggregateEvent t) { - for (TreeItem child : getChildren()) { - final TreeItem findTreeItemForEvent = ((NavTreeItem) child).findTreeItemForEvent(t); + public NavTreeItem findTreeItemForEvent(EventBundle t) { + for (EventTypeTreeItem child : childMap.values()) { + final NavTreeItem findTreeItemForEvent = child.findTreeItemForEvent(t); if (findTreeItemForEvent != null) { return findTreeItemForEvent; } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java index 2ec4b0fa1a..195a286ed9 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/tree/TreeComparator.java @@ -20,27 +20,27 @@ package org.sleuthkit.autopsy.timeline.ui.detailview.tree; import java.util.Comparator; import javafx.scene.control.TreeItem; +import org.sleuthkit.autopsy.timeline.datamodel.EventBundle; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; -enum TreeComparator implements Comparator> { +enum TreeComparator implements Comparator>> { Description { @Override - public int compare(TreeItem o1, TreeItem o2) { + public int compare(TreeItem> o1, TreeItem> o2) { return o1.getValue().getDescription().compareTo(o2.getValue().getDescription()); } }, Count { @Override - public int compare(TreeItem o1, TreeItem o2) { - - return -Integer.compare(o1.getValue().getCount(), o2.getValue().getCount()); + public int compare(TreeItem> o1, TreeItem> o2) { + return Long.compare(o2.getValue().getCount(), o1.getValue().getCount()); } }, Type { @Override - public int compare(TreeItem o1, TreeItem o2) { - return EventType.getComparator().compare(o1.getValue().getType(), o2.getValue().getType()); + public int compare(TreeItem> o1, TreeItem> o2) { + return EventType.getComparator().compare(o1.getValue().getEventType(), o2.getValue().getEventType()); } }; diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterCheckBoxCell.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterCheckBoxCell.java deleted file mode 100644 index d53aadce15..0000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterCheckBoxCell.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2015 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.timeline.ui.filtering; - -import javafx.application.Platform; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.scene.control.CheckBox; -import javafx.scene.control.TreeTableCell; -import org.sleuthkit.autopsy.timeline.filters.AbstractFilter; - -/** - * A {@link TreeTableCell} that represents the active state of a - * {@link AbstractFilter} as a checkbox - */ -class FilterCheckBoxCell extends TreeTableCell { - - private final CheckBox checkBox = new CheckBox(); - private SimpleBooleanProperty activeProperty; - - @Override - protected void updateItem(AbstractFilter item, boolean empty) { - super.updateItem(item, empty); - Platform.runLater(() -> { - if (activeProperty != null) { - checkBox.selectedProperty().unbindBidirectional(activeProperty); - } - checkBox.disableProperty().unbind(); - if (item == null) { - setText(null); - setGraphic(null); - } else { - setText(item.getDisplayName()); - activeProperty = item.getSelectedProperty(); - checkBox.selectedProperty().bindBidirectional(activeProperty); - checkBox.disableProperty().bind(item.getDisabledProperty()); - setGraphic(checkBox); - } - }); - } -} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterCheckBoxCellFactory.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterCheckBoxCellFactory.java new file mode 100644 index 0000000000..535289c6aa --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterCheckBoxCellFactory.java @@ -0,0 +1,55 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.timeline.ui.filtering; + +import java.util.function.Supplier; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.scene.control.CheckBox; +import javafx.scene.control.IndexedCell; +import org.sleuthkit.autopsy.timeline.filters.AbstractFilter; +import org.sleuthkit.autopsy.timeline.ui.AbstractFXCellFactory; + +class FilterCheckBoxCellFactory extends AbstractFXCellFactory { + + private final CheckBox checkBox = new CheckBox(); + private SimpleBooleanProperty selectedProperty; + private SimpleBooleanProperty disabledProperty; + + @Override + protected void configureCell(IndexedCell cell, X item, boolean empty, Supplier supplier) { + if (selectedProperty != null) { + checkBox.selectedProperty().unbindBidirectional(selectedProperty); + } + if (disabledProperty != null) { + checkBox.disableProperty().unbindBidirectional(disabledProperty); + } + + if (item == null) { + cell.setText(null); + cell.setGraphic(null); + } else { + cell.setText(item.getDisplayName()); + selectedProperty = item.selectedProperty(); + checkBox.selectedProperty().bindBidirectional(selectedProperty); + disabledProperty = item.getDisabledProperty(); + checkBox.disableProperty().bindBidirectional(disabledProperty); + cell.setGraphic(checkBox); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.fxml b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.fxml index 265c986360..6f6d17b81a 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.fxml +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.fxml @@ -1,5 +1,6 @@ + @@ -26,13 +27,23 @@ -

- - - - - - - - +
+ + + + + + + + + + + + + + + + + +
diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.java index 3f9bc03fab..93017dcc7c 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterSetPanel.java @@ -22,25 +22,35 @@ import javafx.application.Platform; import javafx.beans.Observable; import javafx.collections.FXCollections; import javafx.collections.ObservableMap; +import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.ContextMenu; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; import javafx.scene.control.Menu; import javafx.scene.control.MenuItem; +import javafx.scene.control.SplitPane; +import javafx.scene.control.TitledPane; import javafx.scene.control.TreeItem; import javafx.scene.control.TreeTableColumn; import javafx.scene.control.TreeTableRow; import javafx.scene.control.TreeTableView; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; import org.controlsfx.control.action.Action; +import org.controlsfx.control.action.ActionUtils; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.timeline.FXMLConstructor; import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.TimeLineView; +import org.sleuthkit.autopsy.timeline.VisualizationMode; import org.sleuthkit.autopsy.timeline.actions.ResetFilters; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.RootEventType; import org.sleuthkit.autopsy.timeline.filters.AbstractFilter; +import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter; import org.sleuthkit.autopsy.timeline.filters.Filter; import org.sleuthkit.autopsy.timeline.filters.RootFilter; import org.sleuthkit.autopsy.timeline.filters.TypeFilter; @@ -54,7 +64,9 @@ import static org.sleuthkit.autopsy.timeline.ui.filtering.Bundle.Timeline_ui_fil * This also implements {@link TimeLineView} since it dynamically updates its * filters based on the contents of a {@link FilteredEventsModel} */ -public class FilterSetPanel extends BorderPane implements TimeLineView { +final public class FilterSetPanel extends BorderPane implements TimeLineView { + + private static final Image TICK = new Image("org/sleuthkit/autopsy/timeline/images/tick.png"); @FXML private Button applyButton; @@ -71,14 +83,22 @@ public class FilterSetPanel extends BorderPane implements TimeLineView { @FXML private TreeTableColumn legendColumn; + @FXML + private ListView hiddenDescriptionsListView; + @FXML + private TitledPane hiddenDescriptionsPane; + @FXML + private SplitPane splitPane; + private FilteredEventsModel filteredEvents; private TimeLineController controller; private final ObservableMap expansionMap = FXCollections.observableHashMap(); + private double position; @FXML - @NbBundle.Messages({"FilterSetPanel.applyButton.text=Apply", + @NbBundle.Messages({ "Timeline.ui.filtering.menuItem.all=all", "FilterSetPanel.defaultButton.text=Default", "Timeline.ui.filtering.menuItem.none=none", @@ -88,10 +108,7 @@ public class FilterSetPanel extends BorderPane implements TimeLineView { void initialize() { assert applyButton != null : "fx:id=\"applyButton\" was not injected: check your FXML file 'FilterSetPanel.fxml'."; // NON-NLS - applyButton.setOnAction(e -> { - controller.pushFilters((RootFilter) filterTreeTable.getRoot().getValue().copyOf()); - }); - applyButton.setText(Bundle.FilterSetPanel_applyButton_text()); + ActionUtils.configureButton(new ApplyFiltersAction(), applyButton); defaultButton.setText(Bundle.FilterSetPanel_defaultButton_text()); //remove column headers via css. @@ -148,7 +165,7 @@ public class FilterSetPanel extends BorderPane implements TimeLineView { //configure tree column to show name of filter and checkbox treeColumn.setCellValueFactory(param -> param.getValue().valueProperty()); - treeColumn.setCellFactory(col -> new FilterCheckBoxCell()); + treeColumn.setCellFactory(col -> new FilterCheckBoxCellFactory<>().forTreeTable(col)); //configure legend column to show legend (or othe supplamantal ui, eg, text field for text filter) legendColumn.setCellValueFactory(param -> param.getValue().valueProperty()); @@ -158,7 +175,7 @@ public class FilterSetPanel extends BorderPane implements TimeLineView { public FilterSetPanel() { FXMLConstructor.construct(this, "FilterSetPanel.fxml"); // NON-NLS - expansionMap.put(new TypeFilter(RootEventType.getInstance()).getDisplayName(), Boolean.TRUE); + expansionMap.put(new TypeFilter(RootEventType.getInstance()).getDisplayName(), true); } @Override @@ -168,20 +185,102 @@ public class FilterSetPanel extends BorderPane implements TimeLineView { defaultButton.setOnAction(defaultFiltersAction); defaultButton.disableProperty().bind(defaultFiltersAction.disabledProperty()); this.setModel(timeLineController.getEventsModel()); + + hiddenDescriptionsListView.setItems(controller.getQuickHideFilters()); + hiddenDescriptionsListView.setCellFactory((ListView param) -> { + final ListCell forList = new FilterCheckBoxCellFactory().forList(); + + forList.itemProperty().addListener((Observable observable) -> { + if (forList.getItem() == null) { + forList.setContextMenu(null); + } else { + forList.setContextMenu(new ContextMenu(new MenuItem() { + { + forList.getItem().selectedProperty().addListener((observable, wasSelected, isSelected) -> { + configureText(isSelected); + }); + + configureText(forList.getItem().selectedProperty().get()); + setOnAction((ActionEvent event) -> { + controller.getQuickHideFilters().remove(forList.getItem()); + }); + } + + private void configureText(Boolean newValue) { + if (newValue) { + setText("Unhide and remove from list"); + } else { + setText("Remove from list"); + } + } + })); + } + }); + + return forList; + }); + + controller.viewModeProperty().addListener(observable -> { + applyFilters(); + if (controller.viewModeProperty().get() == VisualizationMode.COUNTS) { + position = splitPane.getDividerPositions()[0]; + splitPane.setDividerPositions(1); + hiddenDescriptionsPane.setExpanded(false); + hiddenDescriptionsPane.setCollapsible(false); + hiddenDescriptionsPane.setDisable(true); + } else { + splitPane.setDividerPositions(position); + hiddenDescriptionsPane.setDisable(false); + hiddenDescriptionsPane.setCollapsible(true); + hiddenDescriptionsPane.setExpanded(true); + hiddenDescriptionsPane.setCollapsible(false); + + } + }); } @Override + public void setModel(FilteredEventsModel filteredEvents) { this.filteredEvents = filteredEvents; - refresh(); + this.filteredEvents.eventTypeZoomProperty().addListener((Observable observable) -> { + applyFilters(); + }); + this.filteredEvents.descriptionLODProperty().addListener((Observable observable) -> { + applyFilters(); + }); + this.filteredEvents.timeRangeProperty().addListener((Observable observable) -> { + applyFilters(); + }); this.filteredEvents.filterProperty().addListener((Observable o) -> { refresh(); }); + refresh(); + } private void refresh() { Platform.runLater(() -> { - filterTreeTable.setRoot(new FilterTreeItem(filteredEvents.filterProperty().get().copyOf(), expansionMap)); + filterTreeTable.setRoot(new FilterTreeItem(filteredEvents.getFilter().copyOf(), expansionMap)); + }); + } + + @NbBundle.Messages({"FilterSetPanel.applyButton.text=Apply"}) + private class ApplyFiltersAction extends Action { + + ApplyFiltersAction() { + super(Bundle.FilterSetPanel_applyButton_text()); + setLongText("(Re)Apply filters"); + setGraphic(new ImageView(TICK)); + setEventHandler((ActionEvent t) -> { + applyFilters(); + }); + } + } + + private void applyFilters() { + Platform.runLater(() -> { + controller.pushFilters((RootFilter) filterTreeTable.getRoot().getValue()); }); } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterTreeItem.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterTreeItem.java index d1b71e85e4..adc57dc646 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterTreeItem.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/filtering/FilterTreeItem.java @@ -11,7 +11,7 @@ import org.sleuthkit.autopsy.timeline.filters.Filter; /** * A TreeItem for a filter. */ -public class FilterTreeItem extends TreeItem { +final public class FilterTreeItem extends TreeItem { /** * recursively construct a tree of treeitems to parallel the filter tree of @@ -40,16 +40,17 @@ public class FilterTreeItem extends TreeItem { }); if (f instanceof CompoundFilter) { - CompoundFilter cf = (CompoundFilter) f; + CompoundFilter compoundFilter = (CompoundFilter) f; - for (Filter af : cf.getSubFilters()) { - getChildren().add(new FilterTreeItem(af, expansionMap)); + for (Filter subFilter : compoundFilter.getSubFilters()) { + getChildren().add(new FilterTreeItem(subFilter, expansionMap)); } - cf.getSubFilters().addListener((ListChangeListener.Change c) -> { + compoundFilter.getSubFilters().addListener((ListChangeListener.Change c) -> { while (c.next()) { - for (Filter af : c.getAddedSubList()) { - getChildren().add(new FilterTreeItem(af, expansionMap)); + for (Filter subfFilter : c.getAddedSubList()) { + setExpanded(true); + getChildren().add(new FilterTreeItem(subfFilter, expansionMap)); } } }); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLOD.java b/Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLOD.java deleted file mode 100644 index d1bdb4e067..0000000000 --- a/Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLOD.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2013 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.timeline.zooming; - -import org.openide.util.NbBundle; - -/** - * - */ -public enum DescriptionLOD { - - SHORT(NbBundle.getMessage(DescriptionLOD.class, "DescriptionLOD.short")), - MEDIUM(NbBundle.getMessage(DescriptionLOD.class, "DescriptionLOD.medium")), - FULL(NbBundle.getMessage(DescriptionLOD.class, "DescriptionLOD.full")); - - private final String displayName; - - public String getDisplayName() { - return displayName; - } - - private DescriptionLOD(String displayName) { - this.displayName = displayName; - } - - public DescriptionLOD next() { - try { - return values()[ordinal() + 1]; - } catch (ArrayIndexOutOfBoundsException e) { - return null; - } - } - - public DescriptionLOD previous() { - try { - return values()[ordinal() - 1]; - } catch (ArrayIndexOutOfBoundsException e) { - return null; - } - } -} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLoD.java b/Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLoD.java new file mode 100644 index 0000000000..0fab0442c8 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/zooming/DescriptionLoD.java @@ -0,0 +1,88 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 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.timeline.zooming; + +import org.openide.util.NbBundle; + +/** + * Enumeration of all description levels of detail. + */ +public enum DescriptionLoD { + + SHORT(NbBundle.getMessage(DescriptionLoD.class, "DescriptionLOD.short")), + MEDIUM(NbBundle.getMessage(DescriptionLoD.class, "DescriptionLOD.medium")), + FULL(NbBundle.getMessage(DescriptionLoD.class, "DescriptionLOD.full")); + + private final String displayName; + + public String getDisplayName() { + return displayName; + } + + private DescriptionLoD(String displayName) { + this.displayName = displayName; + } + + public DescriptionLoD moreDetailed() { + try { + return values()[ordinal() + 1]; + } catch (ArrayIndexOutOfBoundsException e) { + return FULL; + } + } + + public DescriptionLoD lessDetailed() { + try { + return values()[ordinal() - 1]; + } catch (ArrayIndexOutOfBoundsException e) { + return SHORT; + } + } + + public DescriptionLoD withRelativeDetail(RelativeDetail relativeDetail) { + switch (relativeDetail) { + case EQUAL: + return this; + case MORE: + return moreDetailed(); + case LESS: + return lessDetailed(); + default: + throw new IllegalArgumentException("Unknown RelativeDetail value " + relativeDetail); + } + } + + public RelativeDetail getDetailLevelRelativeTo(DescriptionLoD other) { + int compareTo = this.compareTo(other); + if (compareTo < 0) { + return RelativeDetail.LESS; + } else if (compareTo == 0) { + return RelativeDetail.EQUAL; + } else { + return RelativeDetail.MORE; + } + } + + public enum RelativeDetail { + + EQUAL, + MORE, + LESS; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/zooming/ZoomParams.java b/Core/src/org/sleuthkit/autopsy/timeline/zooming/ZoomParams.java index 2bb4524e66..2bb289f091 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/zooming/ZoomParams.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/zooming/ZoomParams.java @@ -21,7 +21,6 @@ package org.sleuthkit.autopsy.timeline.zooming; import java.util.Objects; import org.joda.time.Interval; import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.timeline.filters.Filter; import org.sleuthkit.autopsy.timeline.filters.RootFilter; /** @@ -36,7 +35,7 @@ public class ZoomParams { private final RootFilter filter; - private final DescriptionLOD descrLOD; + private final DescriptionLoD descrLOD; public Interval getTimeRange() { return timeRange; @@ -50,16 +49,15 @@ public class ZoomParams { return filter; } - public DescriptionLOD getDescriptionLOD() { + public DescriptionLoD getDescriptionLOD() { return descrLOD; } - public ZoomParams(Interval timeRange, EventTypeZoomLevel zoomLevel, RootFilter filter, DescriptionLOD descrLOD) { + public ZoomParams(Interval timeRange, EventTypeZoomLevel zoomLevel, RootFilter filter, DescriptionLoD descrLOD) { this.timeRange = timeRange; this.typeZoomLevel = zoomLevel; this.filter = filter; this.descrLOD = descrLOD; - } public ZoomParams withTimeAndType(Interval timeRange, EventTypeZoomLevel zoomLevel) { @@ -74,7 +72,7 @@ public class ZoomParams { return new ZoomParams(timeRange, typeZoomLevel, filter, descrLOD); } - public ZoomParams withDescrLOD(DescriptionLOD descrLOD) { + public ZoomParams withDescrLOD(DescriptionLoD descrLOD) { return new ZoomParams(timeRange, typeZoomLevel, filter, descrLOD); } @@ -82,7 +80,7 @@ public class ZoomParams { return new ZoomParams(timeRange, typeZoomLevel, filter, descrLOD); } - public boolean hasFilter(Filter filterSet) { + public boolean hasFilter(RootFilter filterSet) { return this.filter.equals(filterSet); } @@ -94,7 +92,7 @@ public class ZoomParams { return this.timeRange == null ? false : this.timeRange.equals(timeRange); } - public boolean hasDescrLOD(DescriptionLOD newLOD) { + public boolean hasDescrLOD(DescriptionLoD newLOD) { return this.descrLOD.equals(newLOD); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/zooming/ZoomSettingsPane.java b/Core/src/org/sleuthkit/autopsy/timeline/zooming/ZoomSettingsPane.java index 1885c90d21..b1c5719625 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/zooming/ZoomSettingsPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/zooming/ZoomSettingsPane.java @@ -97,7 +97,7 @@ public class ZoomSettingsPane extends TitledPane implements TimeLineView { typeZoomSlider.setMin(1); typeZoomSlider.setMax(2); typeZoomSlider.setLabelFormatter(new TypeZoomConverter()); - descrLODSlider.setMax(DescriptionLOD.values().length - 1); + descrLODSlider.setMax(DescriptionLoD.values().length - 1); descrLODSlider.setLabelFormatter(new DescrLODConverter()); descrLODLabel.setText( NbBundle.getMessage(this.getClass(), "ZoomSettingsPane.descrLODLabel.text")); @@ -115,7 +115,7 @@ public class ZoomSettingsPane extends TitledPane implements TimeLineView { synchronized public void setController(TimeLineController controller) { this.controller = controller; setModel(controller.getEventsModel()); - descrLODSlider.disableProperty().bind(controller.getViewMode().isEqualTo(VisualizationMode.COUNTS)); + descrLODSlider.disableProperty().bind(controller.viewModeProperty().isEqualTo(VisualizationMode.COUNTS)); Back back = new Back(controller); backButton.disableProperty().bind(back.disabledProperty()); backButton.setOnAction(back); @@ -154,7 +154,7 @@ public class ZoomSettingsPane extends TitledPane implements TimeLineView { initializeSlider(descrLODSlider, () -> { - DescriptionLOD newLOD = DescriptionLOD.values()[Math.round(descrLODSlider.valueProperty().floatValue())]; + DescriptionLoD newLOD = DescriptionLoD.values()[Math.round(descrLODSlider.valueProperty().floatValue())]; if (controller.pushDescrLOD(newLOD) == false) { descrLODSlider.setValue(new DescrLODConverter().fromString(filteredEvents.getDescriptionLOD().toString())); } @@ -244,12 +244,12 @@ public class ZoomSettingsPane extends TitledPane implements TimeLineView { @Override public String toString(Double object) { - return DescriptionLOD.values()[object.intValue()].getDisplayName(); + return DescriptionLoD.values()[object.intValue()].getDisplayName(); } @Override public Double fromString(String string) { - return new Integer(DescriptionLOD.valueOf(string).ordinal()).doubleValue(); + return new Integer(DescriptionLoD.valueOf(string).ordinal()).doubleValue(); } } } diff --git a/CoreLibs/ivy.xml b/CoreLibs/ivy.xml index 5a6cd9dd17..4ae3cddeaf 100644 --- a/CoreLibs/ivy.xml +++ b/CoreLibs/ivy.xml @@ -1,5 +1,3 @@ - - @@ -18,6 +16,7 @@ + @@ -63,5 +62,6 @@ + diff --git a/CoreLibs/nbproject/project.properties b/CoreLibs/nbproject/project.properties index bb6e8ee7c5..6bb8f91065 100644 --- a/CoreLibs/nbproject/project.properties +++ b/CoreLibs/nbproject/project.properties @@ -20,7 +20,7 @@ file.reference.dom4j-1.6.1.jar=release/modules/ext/dom4j-1.6.1.jar file.reference.geronimo-jms_1.1_spec-1.0.jar=release/modules/ext/geronimo-jms_1.1_spec-1.0.jar file.reference.gson-1.4.jar=release/modules/ext/gson-1.4.jar file.reference.gstreamer-java-1.5.jar=release/modules/ext/gstreamer-java-1.5.jar -file.reference.guava-11.0.2.jar=release/modules/ext/guava-11.0.2.jar +file.reference.guava-18.0.jar=release/modules/ext/guava-18.0.jar file.reference.imageio-bmp-3.1.1.jar=release/modules/ext/imageio-bmp-3.1.1.jar file.reference.imageio-core-3.1.1.jar=release/modules/ext/imageio-core-3.1.1.jar file.reference.imageio-icns-3.1.1.jar=release/modules/ext/imageio-icns-3.1.1.jar @@ -71,13 +71,13 @@ file.reference.xmlbeans-2.3.0.jar=release/modules/ext/xmlbeans-2.3.0.jar javac.source=1.7 javac.compilerargs=-Xlint -Xlint:-serial javadoc.reference.controlsfx-8.40.9.jar=release/modules/ext/controlsfx-8.40.9-javadoc.jar -javadoc.reference.guava-11.0.2.jar=release/modules/ext/guava-11.0.2-javadoc.jar +javadoc.reference.guava-18.0.jar=release/modules/ext/guava-18.0-javadoc.jar javadoc.reference.jfxtras-common-8.0-r4.jar=release/modules/ext/jfxtras-common-8.0-r4-javadoc.jar javadoc.reference.jfxtras-controls-8.0-r4.jar=release/modules/ext/jfxtras-controls-8.0-r4-javadoc.jar javadoc.reference.jfxtras-fxml-8.0-r4.jar=release/modules/ext/jfxtras-fxml-8.0-r4-javadoc.jar nbm.needs.restart=true source.reference.controlsfx-8.40.9.jar=release/modules/ext/controlsfx-8.40.9-sources.jar -source.reference.guava-11.0.2.jar=release/modules/ext/guava-11.0.2-sources.jar +source.reference.guava-18.0.jar=release/modules/ext/guava-18.0-sources.jar source.reference.jfxtras-common-8.0-r4.jar=release/modules/ext/jfxtras-common-8.0-r4-sources.jar source.reference.jfxtras-controls-8.0-r4.jar=release/modules/ext/jfxtras-controls-8.0-r4-sources.jar source.reference.jfxtras-fxml-8.0-r4.jar=release/modules/ext/jfxtras-fxml-8.0-r4-sources.jar diff --git a/CoreLibs/nbproject/project.xml b/CoreLibs/nbproject/project.xml index 5eb250b40c..0f07ff2b83 100644 --- a/CoreLibs/nbproject/project.xml +++ b/CoreLibs/nbproject/project.xml @@ -31,16 +31,19 @@ com.apple.eio com.google.common.annotations com.google.common.base - com.google.common.base.internal com.google.common.cache com.google.common.collect + com.google.common.escape com.google.common.eventbus com.google.common.hash + com.google.common.html com.google.common.io com.google.common.math com.google.common.net com.google.common.primitives + com.google.common.reflect com.google.common.util.concurrent + com.google.common.xml com.google.gson com.google.gson.annotations com.google.gson.reflect @@ -708,10 +711,6 @@ ext/commons-lang3-3.0-sources.jar release/modules/ext/commons-lang3-3.0-sources.jar - - ext/guava-11.0.2.jar - release/modules/ext/guava-11.0.2.jar - ext/mail-1.4.3.jar release/modules/ext/mail-1.4.3.jar @@ -728,6 +727,10 @@ ext/common-lang-3.1.1.jar release/modules/ext/common-lang-3.1.1.jar + + ext/guava-18.0.jar + release/modules/ext/guava-18.0.jar + ext/slf4j-api-1.6.1.jar release/modules/ext/slf4j-api-1.6.1.jar diff --git a/ImageGallery/nbproject/project.xml b/ImageGallery/nbproject/project.xml index ab6cd75280..2a28ce3deb 100644 --- a/ImageGallery/nbproject/project.xml +++ b/ImageGallery/nbproject/project.xml @@ -4,7 +4,7 @@ org.sleuthkit.autopsy.imagegallery - + org.netbeans.api.progress diff --git a/ImageGallery/nbproject/suite.properties b/ImageGallery/nbproject/suite.properties new file mode 100644 index 0000000000..29d7cc9bd6 --- /dev/null +++ b/ImageGallery/nbproject/suite.properties @@ -0,0 +1 @@ +suite.dir=${basedir}/.. diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java index fddef3379c..395349cbce 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java @@ -19,13 +19,13 @@ package org.sleuthkit.autopsy.imagegallery; import java.beans.PropertyChangeEvent; -import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.logging.Level; +import java.util.stream.Collectors; import javafx.application.Platform; import javafx.beans.Observable; import javafx.beans.property.ReadOnlyBooleanProperty; @@ -55,11 +55,11 @@ import org.netbeans.api.progress.ProgressHandle; import org.netbeans.api.progress.ProgressHandleFactory; import org.openide.util.Exceptions; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; +import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.coreutils.History; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; -import org.sleuthkit.autopsy.events.ContentTagAddedEvent; -import org.sleuthkit.autopsy.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableDB; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile; @@ -89,6 +89,8 @@ public final class ImageGalleryController { private static final Logger LOGGER = Logger.getLogger(ImageGalleryController.class.getName()); + private static final String IMAGEGALLERY = "ImageGallery"; + private final Region infoOverLayBackground = new Region() { { setBackground(new Background(new BackgroundFill(Color.GREY, CornerRadii.EMPTY, Insets.EMPTY))); @@ -506,13 +508,13 @@ public final class ImageGalleryController { break; case CONTENT_TAG_ADDED: final ContentTagAddedEvent tagAddedEvent = (ContentTagAddedEvent) evt; - if (getDatabase().isInDB((tagAddedEvent).getTag().getContent().getId())) { + if (getDatabase().isInDB(tagAddedEvent.getAddedTag().getContent().getId())) { getTagsManager().fireTagAddedEvent(tagAddedEvent); } break; case CONTENT_TAG_DELETED: final ContentTagDeletedEvent tagDeletedEvent = (ContentTagDeletedEvent) evt; - if (getDatabase().isInDB((tagDeletedEvent).getTag().getContent().getId())) { + if (getDatabase().isInDB(tagDeletedEvent.getDeletedTagInfo().getContentID())) { getTagsManager().fireTagDeletedEvent(tagDeletedEvent); } break; @@ -846,9 +848,7 @@ public final class ImageGalleryController { } catch (TskCoreException ex) { Logger.getLogger(CopyAnalyzedFiles.class.getName()).log(Level.WARNING, "failed to transfer all database contents", ex); - } catch (IllegalStateException ex) { - Logger.getLogger(CopyAnalyzedFiles.class.getName()).log(Level.SEVERE, "Case was closed out from underneath CopyDataSource task", ex); - } + } progressHandle.finish(); @@ -875,7 +875,7 @@ public final class ImageGalleryController { * check for supported images */ // (name like '.jpg' or name like '.png' ...) - private final String DRAWABLE_QUERY = "(name LIKE '%." + StringUtils.join(FileTypeUtils.getAllSupportedExtensions(), "' or name LIKE '%.") + "') "; + private final String DRAWABLE_QUERY = "(name LIKE '%." + StringUtils.join(FileTypeUtils.getAllSupportedExtensions(), "' OR name LIKE '%.") + "') "; private ProgressHandle progressHandle = ProgressHandleFactory.createHandle("prepopulating image/video database"); @@ -889,29 +889,32 @@ public final class ImageGalleryController { } /** - * Copy files from a newly added data source into the DB + * Copy files from a newly added data source into the DB. Get all + * "drawable" files, based on extension. After ingest we use file type + * id module and if necessary jpeg signature matching to add/remove + * files */ @Override public void run() { progressHandle.start(); updateMessage("prepopulating image/video database"); - /* - * Get all "drawable" files, based on extension. After ingest we use - * file type id module and if necessary jpeg signature matching to - * add/remove files - */ - final List files; try { - List fsObjIds = new ArrayList<>(); - - String fsQuery; + String fsQuery = ""; if (dataSource instanceof Image) { - Image image = (Image) dataSource; - for (FileSystem fs : image.getFileSystems()) { - fsObjIds.add(fs.getId()); + List fileSystems = ((Image) dataSource).getFileSystems(); + if (fileSystems.isEmpty() == false) { + /* + * no filesystems, don't bother with the initial + * population, just catch things on file_done + */ + progressHandle.finish(); + return; } - fsQuery = "(fs_obj_id = " + StringUtils.join(fsObjIds, " or fs_obj_id = ") + ") "; + String internal = fileSystems.stream() + .map(fileSystem -> String.valueOf(fileSystem.getId())) + .collect(Collectors.joining(" OR fs_obj_id = ")); + fsQuery = "(fs_obj_id = " + internal + ") "; //suffix } else { /* * NOTE: Logical files currently (Apr '15) have a null value @@ -923,7 +926,7 @@ public final class ImageGalleryController { fsQuery = "(fs_obj_id IS NULL) "; } - files = getSleuthKitCase().findAllFilesWhere(fsQuery + " and " + DRAWABLE_QUERY); + final List files = getSleuthKitCase().findAllFilesWhere(fsQuery + " AND " + DRAWABLE_QUERY); progressHandle.switchToDeterminate(files.size()); //do in transaction @@ -948,9 +951,7 @@ public final class ImageGalleryController { } catch (TskCoreException ex) { Logger.getLogger(PrePopulateDataSourceFiles.class.getName()).log(Level.WARNING, "failed to transfer all database contents", ex); - } catch (IllegalStateException | NullPointerException ex) { - Logger.getLogger(PrePopulateDataSourceFiles.class.getName()).log(Level.WARNING, "Case was closed out from underneath prepopulating database"); - } + } progressHandle.finish(); } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryModule.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryModule.java index 176086d960..ef4248f104 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryModule.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryModule.java @@ -61,7 +61,7 @@ public class ImageGalleryModule { * @return the Path to the ModuleOuput subfolder for Image Gallery */ static Path getModuleOutputDir(Case theCase) { - return Paths.get(theCase.getModulesOutputDirAbsPath(), getModuleName()); + return Paths.get(theCase.getModuleDirectory(), getModuleName()); } /** provides static utilities, can not be instantiated */ diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryOptionsPanelController.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryOptionsPanelController.java index 76ca06ed19..78ed75fe6d 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryOptionsPanelController.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryOptionsPanelController.java @@ -33,7 +33,8 @@ import org.openide.util.Lookup; categoryName = "#OptionsCategory_Name_Options", iconBase = "org/sleuthkit/autopsy/imagegallery/images/polaroid_48_silhouette.png", keywords = "#OptionsCategory_Keywords_Options", - keywordsCategory = "Options" + keywordsCategory = "Options", + position = 10 ) @org.openide.util.NbBundle.Messages({"OptionsCategory_Name_Options=Image / Video Gallery", "OptionsCategory_Keywords_Options=image video gallery category "}) public final class ImageGalleryOptionsPanelController extends OptionsPanelController { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/PerCaseProperties.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/PerCaseProperties.java index c8d1c66410..75a35fc798 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/PerCaseProperties.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/PerCaseProperties.java @@ -88,7 +88,7 @@ class PerCaseProperties { * @return true if the config exists, false otherwise. */ public synchronized boolean configExists(String moduleName) { - Path get = Paths.get(theCase.getModulesOutputDirAbsPath(), moduleName, theCase.getName() + ".properties"); + Path get = Paths.get(theCase.getModuleDirectory(), moduleName, theCase.getName() + ".properties"); return Files.exists(get); } @@ -114,7 +114,7 @@ class PerCaseProperties { * file doesn't exist. */ private synchronized Path getPropertyPath(String moduleName) { - return Paths.get(theCase.getModulesOutputDirAbsPath(), moduleName, theCase.getName() + ".properties"); //NON-NLS + return Paths.get(theCase.getModuleDirectory(), moduleName, theCase.getName() + ".properties"); //NON-NLS } /** diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/CategoryManager.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/CategoryManager.java index f6ab66fa04..8972e192b0 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/CategoryManager.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/CategoryManager.java @@ -31,8 +31,8 @@ import java.util.logging.Level; import javax.annotation.concurrent.Immutable; import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.events.ContentTagAddedEvent; -import org.sleuthkit.autopsy.events.ContentTagDeletedEvent; +import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; +import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.TagName; @@ -52,12 +52,10 @@ import org.sleuthkit.datamodel.TskCoreException; */ public class CategoryManager { - private static final java.util.logging.Logger LOGGER = Logger.getLogger(CategoryManager.class.getName()); + private static final org.sleuthkit.autopsy.coreutils.Logger LOGGER = Logger.getLogger(CategoryManager.class.getName()); private final ImageGalleryController controller; - - /** * the DrawableDB that backs the category counts cache. The counts are * initialized from this, and the counting of CAT-0 is always delegated to @@ -234,7 +232,7 @@ public class CategoryManager { @Subscribe public void handleTagAdded(ContentTagAddedEvent event) { - final ContentTag addedTag = event.getTag(); + final ContentTag addedTag = event.getAddedTag(); if (isCategoryTagName(addedTag.getName())) { final DrawableTagsManager tagsManager = controller.getTagsManager(); try { @@ -263,14 +261,15 @@ public class CategoryManager { @Subscribe public void handleTagDeleted(ContentTagDeletedEvent event) { - ContentTag deleted = event.getTag(); - if (isCategoryTagName(deleted.getName())) { + final ContentTagDeletedEvent.DeletedContentTagInfo deletedTagInfo = event.getDeletedTagInfo(); + TagName tagName = deletedTagInfo.getName(); + if (isCategoryTagName(tagName)) { - Category deletedCat = CategoryManager.categoryFromTagName(deleted.getName()); + Category deletedCat = CategoryManager.categoryFromTagName(tagName); if (deletedCat != Category.ZERO) { decrementCategoryCount(deletedCat); } - fireChange(Collections.singleton(deleted.getContent().getId()), null); + fireChange(Collections.singleton(deletedTagInfo.getContentID()), null); } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java index c23afe43d8..4aaf90735d 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java @@ -72,7 +72,7 @@ import org.sqlite.SQLiteJDBCLoader; */ public final class DrawableDB { - private static final java.util.logging.Logger LOGGER = Logger.getLogger(DrawableDB.class.getName()); + private static final org.sleuthkit.autopsy.coreutils.Logger LOGGER = Logger.getLogger(DrawableDB.class.getName()); //column name constants////////////////////// private static final String ANALYZED = "analyzed"; @@ -205,27 +205,27 @@ public final class DrawableDB { "INSERT OR IGNORE INTO drawable_files (obj_id , path, name, created_time, modified_time, make, model, analyzed) " + "VALUES (?,?,?,?,?,?,?,?)"); - removeFileStmt = prepareStatement("delete from drawable_files where obj_id = ?"); + removeFileStmt = prepareStatement("DELETE FROM drawable_files WHERE obj_id = ?"); - pathGroupStmt = prepareStatement("select obj_id , analyzed from drawable_files where path = ? ", DrawableAttribute.PATH); - nameGroupStmt = prepareStatement("select obj_id , analyzed from drawable_files where name = ? ", DrawableAttribute.NAME); - created_timeGroupStmt = prepareStatement("select obj_id , analyzed from drawable_files where created_time = ? ", DrawableAttribute.CREATED_TIME); - modified_timeGroupStmt = prepareStatement("select obj_id , analyzed from drawable_files where modified_time = ? ", DrawableAttribute.MODIFIED_TIME); - makeGroupStmt = prepareStatement("select obj_id , analyzed from drawable_files where make = ? ", DrawableAttribute.MAKE); - modelGroupStmt = prepareStatement("select obj_id , analyzed from drawable_files where model = ? ", DrawableAttribute.MODEL); - analyzedGroupStmt = prepareStatement("Select obj_id , analyzed from drawable_files where analyzed = ?", DrawableAttribute.ANALYZED); - hashSetGroupStmt = prepareStatement("select drawable_files.obj_id as obj_id, analyzed from drawable_files , hash_sets , hash_set_hits where drawable_files.obj_id = hash_set_hits.obj_id and hash_sets.hash_set_id = hash_set_hits.hash_set_id and hash_sets.hash_set_name = ?", DrawableAttribute.HASHSET); + pathGroupStmt = prepareStatement("SELECT obj_id , analyzed FROM drawable_files WHERE path = ? ", DrawableAttribute.PATH); + nameGroupStmt = prepareStatement("SELECT obj_id , analyzed FROM drawable_files WHERE name = ? ", DrawableAttribute.NAME); + created_timeGroupStmt = prepareStatement("SELECT obj_id , analyzed FROM drawable_files WHERE created_time = ? ", DrawableAttribute.CREATED_TIME); + modified_timeGroupStmt = prepareStatement("SELECT obj_id , analyzed FROM drawable_files WHERE modified_time = ? ", DrawableAttribute.MODIFIED_TIME); + makeGroupStmt = prepareStatement("SELECT obj_id , analyzed FROM drawable_files WHERE make = ? ", DrawableAttribute.MAKE); + modelGroupStmt = prepareStatement("SELECT obj_id , analyzed FROM drawable_files WHERE model = ? ", DrawableAttribute.MODEL); + analyzedGroupStmt = prepareStatement("SELECT obj_id , analyzed FROM drawable_files WHERE analyzed = ?", DrawableAttribute.ANALYZED); + hashSetGroupStmt = prepareStatement("SELECT drawable_files.obj_id AS obj_id, analyzed FROM drawable_files , hash_sets , hash_set_hits WHERE drawable_files.obj_id = hash_set_hits.obj_id AND hash_sets.hash_set_id = hash_set_hits.hash_set_id AND hash_sets.hash_set_name = ?", DrawableAttribute.HASHSET); updateGroupStmt = prepareStatement("insert or replace into groups (seen, value, attribute) values( ?, ? , ?)"); insertGroupStmt = prepareStatement("insert or ignore into groups (value, attribute) values (?,?)"); - groupSeenQueryStmt = prepareStatement("select seen from groups where value = ? and attribute = ?"); + groupSeenQueryStmt = prepareStatement("SELECT seen FROM groups WHERE value = ? AND attribute = ?"); selectHashSetNamesStmt = prepareStatement("SELECT DISTINCT hash_set_name FROM hash_sets"); - insertHashSetStmt = prepareStatement("insert or ignore into hash_sets (hash_set_name) values (?)"); - selectHashSetStmt = prepareStatement("select hash_set_id from hash_sets where hash_set_name = ?"); + insertHashSetStmt = prepareStatement("INSERT OR IGNORE INTO hash_sets (hash_set_name) VALUES (?)"); + selectHashSetStmt = prepareStatement("SELECT hash_set_id FROM hash_sets WHERE hash_set_name = ?"); - insertHashHitStmt = prepareStatement("insert or ignore into hash_set_hits (hash_set_id, obj_id) values (?,?)"); + insertHashHitStmt = prepareStatement("INSERT OR IGNORE INTO hash_set_hits (hash_set_id, obj_id) VALUES (?,?)"); for (Category cat : Category.values()) { insertGroup(cat.getDisplayName(), DrawableAttribute.CATEGORY); @@ -647,7 +647,7 @@ public final class DrawableDB { public Boolean isFileAnalyzed(long fileId) { dbReadLock(); try (Statement stmt = con.createStatement(); - ResultSet analyzedQuery = stmt.executeQuery("select analyzed from drawable_files where obj_id = " + fileId)) { + ResultSet analyzedQuery = stmt.executeQuery("SELECT analyzed FROM drawable_files WHERE obj_id = " + fileId)) { while (analyzedQuery.next()) { return analyzedQuery.getBoolean(ANALYZED); } @@ -665,7 +665,7 @@ public final class DrawableDB { dbReadLock(); try (Statement stmt = con.createStatement(); //Can't make this a preprared statement because of the IN ( ... ) - ResultSet analyzedQuery = stmt.executeQuery("select count(analyzed) as analyzed from drawable_files where analyzed = 1 and obj_id in (" + StringUtils.join(fileIds, ", ") + ")")) { + ResultSet analyzedQuery = stmt.executeQuery("SELECT COUNT(analyzed) AS analyzed FROM drawable_files WHERE analyzed = 1 AND obj_id IN (" + StringUtils.join(fileIds, ", ") + ")")) { while (analyzedQuery.next()) { return analyzedQuery.getInt(ANALYZED) == fileIds.size(); } @@ -687,7 +687,7 @@ public final class DrawableDB { // In testing, this method appears to be a lot faster than doing one large select statement for (Long fileID : fileIDsInGroup) { Statement stmt = con.createStatement(); - ResultSet analyzedQuery = stmt.executeQuery("select analyzed from drawable_files where obj_id = " + fileID); + ResultSet analyzedQuery = stmt.executeQuery("SELECT analyzed FROM drawable_files WHERE obj_id = " + fileID); while (analyzedQuery.next()) { if (analyzedQuery.getInt(ANALYZED) == 0) { return false; @@ -863,15 +863,15 @@ public final class DrawableDB { default: dbReadLock(); //TODO: convert this to prepared statement - StringBuilder query = new StringBuilder("select " + groupBy.attrName.toString() + ", count(*) from drawable_files group by " + groupBy.attrName.toString()); + StringBuilder query = new StringBuilder("SELECT " + groupBy.attrName.toString() + ", COUNT(*) FROM drawable_files GROUP BY " + groupBy.attrName.toString()); String orderByClause = ""; switch (sortBy) { case GROUP_BY_VALUE: - orderByClause = " order by " + groupBy.attrName.toString(); + orderByClause = " ORDER BY " + groupBy.attrName.toString(); break; case FILE_COUNT: - orderByClause = " order by count(*)"; + orderByClause = " ORDER BY COUNT(*)"; break; case NONE: // case PRIORITY: @@ -1063,7 +1063,7 @@ public final class DrawableDB { public int countAllFiles() { int result = -1; dbReadLock(); - try (ResultSet rs = con.createStatement().executeQuery("select count(*) as COUNT from drawable_files")) { + try (ResultSet rs = con.createStatement().executeQuery("SELECT COUNT(*) AS COUNT FROM drawable_files")) { while (rs.next()) { result = rs.getInt("COUNT"); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableTagsManager.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableTagsManager.java index 2c29f44d85..8e7c1e8503 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableTagsManager.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableTagsManager.java @@ -31,8 +31,8 @@ import javax.annotation.Nonnull; import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.sleuthkit.autopsy.casemodule.services.TagsManager; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.events.ContentTagAddedEvent; -import org.sleuthkit.autopsy.events.ContentTagDeletedEvent; +import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; +import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.TagName; diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java index 41cd5d63a3..0a804118f8 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java @@ -34,7 +34,7 @@ import java.util.TreeSet; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.logging.Level; -import java.util.logging.Logger; +import org.sleuthkit.autopsy.coreutils.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; import javafx.application.Platform; @@ -61,8 +61,8 @@ import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.LoggedTask; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.coreutils.ThreadConfined.ThreadType; -import org.sleuthkit.autopsy.events.ContentTagAddedEvent; -import org.sleuthkit.autopsy.events.ContentTagDeletedEvent; +import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; +import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.datamodel.Category; import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager; @@ -492,16 +492,16 @@ public class GroupManager { @Subscribe public void handleTagAdded(ContentTagAddedEvent evt) { GroupKey newGroupKey = null; - final long fileID = evt.getTag().getContent().getId(); - if (groupBy == DrawableAttribute.CATEGORY && CategoryManager.isCategoryTagName(evt.getTag().getName())) { - newGroupKey = new GroupKey(DrawableAttribute.CATEGORY, CategoryManager.categoryFromTagName(evt.getTag().getName())); + final long fileID = evt.getAddedTag().getContent().getId(); + if (groupBy == DrawableAttribute.CATEGORY && CategoryManager.isCategoryTagName(evt.getAddedTag().getName())) { + newGroupKey = new GroupKey(DrawableAttribute.CATEGORY, CategoryManager.categoryFromTagName(evt.getAddedTag().getName())); for (GroupKey oldGroupKey : groupMap.keySet()) { if (oldGroupKey.equals(newGroupKey) == false) { removeFromGroup(oldGroupKey, fileID); } } - } else if (groupBy == DrawableAttribute.TAGS && CategoryManager.isNotCategoryTagName(evt.getTag().getName())) { - newGroupKey = new GroupKey(DrawableAttribute.TAGS, evt.getTag().getName()); + } else if (groupBy == DrawableAttribute.TAGS && CategoryManager.isNotCategoryTagName(evt.getAddedTag().getName())) { + newGroupKey = new GroupKey(DrawableAttribute.TAGS, evt.getAddedTag().getName()); } if (newGroupKey != null) { DrawableGroup g = getGroupForKey(newGroupKey); @@ -528,13 +528,15 @@ public class GroupManager { @Subscribe public void handleTagDeleted(ContentTagDeletedEvent evt) { GroupKey groupKey = null; - if (groupBy == DrawableAttribute.CATEGORY && CategoryManager.isCategoryTagName(evt.getTag().getName())) { - groupKey = new GroupKey(DrawableAttribute.CATEGORY, CategoryManager.categoryFromTagName(evt.getTag().getName())); - } else if (groupBy == DrawableAttribute.TAGS && CategoryManager.isNotCategoryTagName(evt.getTag().getName())) { - groupKey = new GroupKey(DrawableAttribute.TAGS, evt.getTag().getName()); + final ContentTagDeletedEvent.DeletedContentTagInfo deletedTagInfo = evt.getDeletedTagInfo(); + final TagName tagName = deletedTagInfo.getName(); + if (groupBy == DrawableAttribute.CATEGORY && CategoryManager.isCategoryTagName(tagName)) { + groupKey = new GroupKey(DrawableAttribute.CATEGORY, CategoryManager.categoryFromTagName(tagName)); + } else if (groupBy == DrawableAttribute.TAGS && CategoryManager.isNotCategoryTagName(tagName)) { + groupKey = new GroupKey(DrawableAttribute.TAGS, tagName); } if (groupKey != null) { - final long fileID = evt.getTag().getContent().getId(); + final long fileID = deletedTagInfo.getContentID(); DrawableGroup g = removeFromGroup(groupKey, fileID); } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/Toolbar.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/Toolbar.java index 8f1f4466a2..a30238eb58 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/Toolbar.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/Toolbar.java @@ -20,7 +20,7 @@ package org.sleuthkit.autopsy.imagegallery.gui; import java.util.List; import java.util.logging.Level; -import java.util.logging.Logger; +import org.sleuthkit.autopsy.coreutils.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; import javafx.application.Platform; diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableTileBase.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableTileBase.java index 1bb1aa2ff2..545eb25edb 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableTileBase.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableTileBase.java @@ -58,9 +58,8 @@ import org.sleuthkit.autopsy.datamodel.FileNode; import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; import org.sleuthkit.autopsy.directorytree.NewWindowViewAction; -import org.sleuthkit.autopsy.events.ContentTagAddedEvent; -import org.sleuthkit.autopsy.events.ContentTagDeletedEvent; -import org.sleuthkit.autopsy.events.TagEvent; +import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; +import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.imagegallery.FileIDSelectionModel; import org.sleuthkit.autopsy.imagegallery.ImageGalleryTopComponent; import org.sleuthkit.autopsy.imagegallery.actions.AddDrawableTagAction; @@ -343,35 +342,38 @@ public abstract class DrawableTileBase extends DrawableUIBase { @Subscribe @Override public void handleTagAdded(ContentTagAddedEvent evt) { - handleTagEvent(evt, () -> { - Platform.runLater(() -> { - followUpImageView.setImage(followUpIcon); - followUpToggle.setSelected(true); - }); + getFileID().ifPresent((fileID) -> { + try { + final TagName followUpTagName = getController().getTagsManager().getFollowUpTagName(); + final ContentTag addedTag = evt.getAddedTag(); + if (fileID == addedTag.getContent().getId() + && addedTag.getName().equals(followUpTagName)) { + Platform.runLater(() -> { + followUpImageView.setImage(followUpIcon); + followUpToggle.setSelected(true); + }); + } + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "Failed to get followup tag name. Unable to update follow up status for file. ", ex); + } }); } @Subscribe @Override public void handleTagDeleted(ContentTagDeletedEvent evt) { - handleTagEvent(evt, this::updateFollowUpIcon); - } - - void handleTagEvent(TagEvent evt, Runnable runnable) { - getFileID().ifPresent(fileID -> { + getFileID().ifPresent((fileID) -> { try { final TagName followUpTagName = getController().getTagsManager().getFollowUpTagName(); - final ContentTag deletedTag = evt.getTag(); - - if (fileID == deletedTag.getContent().getId() - && deletedTag.getName().equals(followUpTagName)) { - runnable.run(); + final ContentTagDeletedEvent.DeletedContentTagInfo deletedTagInfo = evt.getDeletedTagInfo(); + if (fileID == deletedTagInfo.getContentID() + && deletedTagInfo.getName().equals(followUpTagName)) { + updateFollowUpIcon(); } } catch (TskCoreException ex) { LOGGER.log(Level.SEVERE, "Failed to get followup tag name. Unable to update follow up status for file. ", ex); } }); - } private void updateFollowUpIcon() { diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableView.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableView.java index 1a1f8433f7..1eda43f9cc 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableView.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/DrawableView.java @@ -14,8 +14,8 @@ import javafx.scene.layout.Region; import javafx.scene.paint.Color; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; -import org.sleuthkit.autopsy.events.ContentTagAddedEvent; -import org.sleuthkit.autopsy.events.ContentTagDeletedEvent; +import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; +import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.datamodel.Category; import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager; diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/MetaDataPane.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/MetaDataPane.java index b86e134f3a..f0a828f644 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/MetaDataPane.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/MetaDataPane.java @@ -27,7 +27,7 @@ import java.util.Objects; import static java.util.Objects.isNull; import static java.util.Objects.nonNull; import java.util.Optional; -import java.util.logging.Logger; +import org.sleuthkit.autopsy.coreutils.Logger; import java.util.stream.Collectors; import javafx.application.Platform; import javafx.beans.property.SimpleObjectProperty; @@ -50,16 +50,14 @@ import javafx.scene.input.KeyEvent; import javafx.scene.layout.Region; import javafx.scene.text.Text; import javafx.util.Pair; -import org.sleuthkit.autopsy.events.ContentTagAddedEvent; -import org.sleuthkit.autopsy.events.ContentTagDeletedEvent; -import org.sleuthkit.autopsy.events.TagEvent; +import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; +import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.imagegallery.FXMLConstructor; import org.sleuthkit.autopsy.imagegallery.ImageGalleryController; import org.sleuthkit.autopsy.imagegallery.datamodel.Category; import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableAttribute; import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile; -import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.TagName; /** @@ -168,7 +166,7 @@ public class MetaDataPane extends DrawableUIBase { } @Override - synchronized protected void setFileHelper(Long newFileID) { + protected synchronized void setFileHelper(Long newFileID) { setFileIDOpt(Optional.ofNullable(newFileID)); if (newFileID == null) { Platform.runLater(() -> { @@ -220,23 +218,18 @@ public class MetaDataPane extends DrawableUIBase { @Subscribe @Override public void handleTagAdded(ContentTagAddedEvent evt) { - handleTagEvent(evt, this::updateUI); + getFileID().ifPresent((fileID) -> { + if (Objects.equals(evt.getAddedTag().getContent().getId(), fileID)) { + updateUI(); + } + }); } @Override public void handleTagDeleted(ContentTagDeletedEvent evt) { - handleTagEvent(evt, this::updateUI); - } - - /** - * - * @param tagFileID the value of tagEvent - * @param runnable the value of runnable - */ - void handleTagEvent(TagEvent tagEvent, final Runnable runnable) { - getFileID().ifPresent(fileID -> { - if (Objects.equals(tagEvent.getTag().getContent().getId(), fileID)) { - runnable.run(); + getFileID().ifPresent((fileID) -> { + if (Objects.equals(evt.getDeletedTagInfo().getContentID(), fileID)) { + updateUI(); } }); } diff --git a/KeywordSearch/build.xml b/KeywordSearch/build.xml index cfbffe2dfd..e70cf6a81e 100644 --- a/KeywordSearch/build.xml +++ b/KeywordSearch/build.xml @@ -45,7 +45,13 @@ + + + + + + diff --git a/KeywordSearch/ivy.xml b/KeywordSearch/ivy.xml index b4dbcb7ed9..c5d193dff8 100644 --- a/KeywordSearch/ivy.xml +++ b/KeywordSearch/ivy.xml @@ -10,21 +10,35 @@ + + - - - + + + + + + - + + + - - - - - - + + + + + + + + + + + + diff --git a/KeywordSearch/ivysettings.xml b/KeywordSearch/ivysettings.xml new file mode 100644 index 0000000000..c27d905255 --- /dev/null +++ b/KeywordSearch/ivysettings.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/KeywordSearch/nbproject/project.xml b/KeywordSearch/nbproject/project.xml index 6a540e5835..0f823bd9be 100644 --- a/KeywordSearch/nbproject/project.xml +++ b/KeywordSearch/nbproject/project.xml @@ -165,249 +165,221 @@ org.apache.tika.utils org.sleuthkit.autopsy.keywordsearch - - ext/metadata-extractor-2.4.0-beta-1.jar - release/modules/ext/metadata-extractor-2.4.0-beta-1.jar - - - ext/commons-io-2.1.jar - release/modules/ext/commons-io-2.1.jar - - - ext/commons-lang-2.4.jar - release/modules/ext/commons-lang-2.4.jar - - - ext/log4j-1.2.17.jar - release/modules/ext/log4j-1.2.17.jar - - - ext/jcl-over-slf4j-1.6.4.jar - release/modules/ext/jcl-over-slf4j-1.6.4.jar - - - ext/asm-all-3.1.jar - release/modules/ext/asm-all-3.1.jar - - - ext/qdox-1.12.jar - release/modules/ext/qdox-1.12.jar - - - ext/org.apache.felix.scr.generator-1.1.2.jar - release/modules/ext/org.apache.felix.scr.generator-1.1.2.jar - ext/bcmail-jdk15-1.45.jar release/modules/ext/bcmail-jdk15-1.45.jar - ext/vorbis-java-core-0.1-tests.jar - release/modules/ext/vorbis-java-core-0.1-tests.jar - - - ext/tika-parsers-1.2-javadoc.jar - release/modules/ext/tika-parsers-1.2-javadoc.jar - - - ext/log4j-over-slf4j-1.6.4.jar - release/modules/ext/log4j-over-slf4j-1.6.4.jar - - - ext/vorbis-java-tika-0.1.jar - release/modules/ext/vorbis-java-tika-0.1.jar - - - ext/isoparser-1.0-RC-1.jar - release/modules/ext/isoparser-1.0-RC-1.jar - - - ext/httpcore-4.1.4.jar - release/modules/ext/httpcore-4.1.4.jar - - - ext/tika-parsers-1.2-sources.jar - release/modules/ext/tika-parsers-1.2-sources.jar - - - ext/aspectjrt-1.6.11.jar - release/modules/ext/aspectjrt-1.6.11.jar - - - ext/commons-compress-1.4.1.jar - release/modules/ext/commons-compress-1.4.1.jar - - - ext/poi-3.8.jar - release/modules/ext/poi-3.8.jar - - - ext/tika-parsers-1.2.jar - release/modules/ext/tika-parsers-1.2.jar - - - ext/apache-mime4j-core-0.7.2.jar - release/modules/ext/apache-mime4j-core-0.7.2.jar - - - ext/rome-0.9.jar - release/modules/ext/rome-0.9.jar - - - ext/httpclient-4.1.3.jar - release/modules/ext/httpclient-4.1.3.jar - - - ext/icu4j-3.8.jar - release/modules/ext/icu4j-3.8.jar - - - ext/juniversalchardet-1.0.3.jar - release/modules/ext/juniversalchardet-1.0.3.jar - - - ext/pdfbox-1.7.0.jar - release/modules/ext/pdfbox-1.7.0.jar - - - ext/jericho-html-3.3-sources.jar - release/modules/ext/jericho-html-3.3-sources.jar - - - ext/jdom-1.0.jar - release/modules/ext/jdom-1.0.jar - - - ext/commons-logging-1.1.1.jar - release/modules/ext/commons-logging-1.1.1.jar - - - ext/tagsoup-1.2.1.jar - release/modules/ext/tagsoup-1.2.1.jar - - - ext/fontbox-1.7.0.jar - release/modules/ext/fontbox-1.7.0.jar - - - ext/poi-ooxml-3.8.jar - release/modules/ext/poi-ooxml-3.8.jar - - - ext/boilerpipe-1.1.0.jar - release/modules/ext/boilerpipe-1.1.0.jar - - - ext/org.osgi.compendium-4.0.0.jar - release/modules/ext/org.osgi.compendium-4.0.0.jar - - - ext/slf4j-api-1.7.2.jar - release/modules/ext/slf4j-api-1.7.2.jar - - - ext/commons-lang-2.4-javadoc.jar - release/modules/ext/commons-lang-2.4-javadoc.jar - - - ext/jempbox-1.7.0.jar - release/modules/ext/jempbox-1.7.0.jar - - - ext/jericho-html-3.3-javadoc.jar - release/modules/ext/jericho-html-3.3-javadoc.jar - - - ext/wstx-asl-3.2.7.jar - release/modules/ext/wstx-asl-3.2.7.jar - - - ext/netcdf-4.2-min.jar - release/modules/ext/netcdf-4.2-min.jar - - - ext/solr-solrj-4.0.0-javadoc.jar - release/modules/ext/solr-solrj-4.0.0-javadoc.jar - - - ext/xmlbeans-2.3.0.jar - release/modules/ext/xmlbeans-2.3.0.jar - - - ext/httpmime-4.1.3.jar - release/modules/ext/httpmime-4.1.3.jar - - - ext/org.osgi.core-4.0.0.jar - release/modules/ext/org.osgi.core-4.0.0.jar - - - ext/org.apache.felix.scr.annotations-1.6.0.jar - release/modules/ext/org.apache.felix.scr.annotations-1.6.0.jar - - - ext/commons-logging-api-1.1.jar - release/modules/ext/commons-logging-api-1.1.jar - - - ext/xz-1.0.jar - release/modules/ext/xz-1.0.jar - - - ext/commons-codec-1.7.jar - release/modules/ext/commons-codec-1.7.jar - - - ext/tika-core-1.2.jar - release/modules/ext/tika-core-1.2.jar - - - ext/zookeeper-3.3.6.jar - release/modules/ext/zookeeper-3.3.6.jar - - - ext/dom4j-1.6.1.jar - release/modules/ext/dom4j-1.6.1.jar - - - ext/poi-scratchpad-3.8.jar - release/modules/ext/poi-scratchpad-3.8.jar - - - ext/poi-ooxml-schemas-3.8.jar - release/modules/ext/poi-ooxml-schemas-3.8.jar - - - ext/bcprov-jdk15-1.45.jar - release/modules/ext/bcprov-jdk15-1.45.jar + ext/tika-parsers-1.5.jar + release/modules/ext/tika-parsers-1.5.jar ext/jericho-html-3.3.jar release/modules/ext/jericho-html-3.3.jar - ext/solr-solrj-4.0.0.jar - release/modules/ext/solr-solrj-4.0.0.jar + ext/org.osgi.compendium-4.0.0.jar + release/modules/ext/org.osgi.compendium-4.0.0.jar + + + ext/slf4j-api-1.7.6.jar + release/modules/ext/slf4j-api-1.7.6.jar + + + ext/xmlbeans-2.3.0.jar + release/modules/ext/xmlbeans-2.3.0.jar + + + ext/jempbox-1.8.4.jar + release/modules/ext/jempbox-1.8.4.jar + + + ext/poi-ooxml-3.10-beta2.jar + release/modules/ext/poi-ooxml-3.10-beta2.jar + + + ext/commons-logging-api-1.1.jar + release/modules/ext/commons-logging-api-1.1.jar + + + ext/asm-all-3.1.jar + release/modules/ext/asm-all-3.1.jar + + + ext/solr-solrj-4.9.1-javadoc.jar + release/modules/ext/solr-solrj-4.9.1-javadoc.jar + + + ext/poi-3.10-beta2.jar + release/modules/ext/poi-3.10-beta2.jar + + + ext/icu4j-3.8.jar + release/modules/ext/icu4j-3.8.jar + + + ext/fontbox-1.8.4.jar + release/modules/ext/fontbox-1.8.4.jar + + + ext/xmpcore-5.1.2.jar + release/modules/ext/xmpcore-5.1.2.jar + + + ext/metadata-extractor-2.7.2.jar + release/modules/ext/metadata-extractor-2.7.2.jar + + + ext/tagsoup-1.2.1.jar + release/modules/ext/tagsoup-1.2.1.jar + + + ext/commons-compress-1.5.jar + release/modules/ext/commons-compress-1.5.jar + + + ext/commons-lang-2.4-javadoc.jar + release/modules/ext/commons-lang-2.4-javadoc.jar + + + ext/org.osgi.core-4.0.0.jar + release/modules/ext/org.osgi.core-4.0.0.jar + + + ext/httpclient-4.3.1.jar + release/modules/ext/httpclient-4.3.1.jar + + + ext/isoparser-1.0-RC-1.jar + release/modules/ext/isoparser-1.0-RC-1.jar + + + ext/log4j-1.2.17.jar + release/modules/ext/log4j-1.2.17.jar ext/commons-lang-2.4-sources.jar release/modules/ext/commons-lang-2.4-sources.jar - ext/solr-solrj-4.0.0-sources.jar - release/modules/ext/solr-solrj-4.0.0-sources.jar + ext/tika-core-1.5.jar + release/modules/ext/tika-core-1.5.jar + + + ext/jericho-html-3.3-javadoc.jar + release/modules/ext/jericho-html-3.3-javadoc.jar ext/apache-mime4j-dom-0.7.2.jar release/modules/ext/apache-mime4j-dom-0.7.2.jar + + ext/org.apache.felix.scr.generator-1.1.2.jar + release/modules/ext/org.apache.felix.scr.generator-1.1.2.jar + + + ext/solr-solrj-4.9.1.jar + release/modules/ext/solr-solrj-4.9.1.jar + + + ext/poi-scratchpad-3.10-beta2.jar + release/modules/ext/poi-scratchpad-3.10-beta2.jar + + + ext/commons-logging-1.1.1.jar + release/modules/ext/commons-logging-1.1.1.jar + + + ext/netcdf-4.2-min.jar + release/modules/ext/netcdf-4.2-min.jar + + + ext/vorbis-java-core-0.1-tests.jar + release/modules/ext/vorbis-java-core-0.1-tests.jar + + + ext/commons-io-2.3.jar + release/modules/ext/commons-io-2.3.jar + + + ext/jericho-html-3.3-sources.jar + release/modules/ext/jericho-html-3.3-sources.jar + + + ext/solr-solrj-4.9.1-sources.jar + release/modules/ext/solr-solrj-4.9.1-sources.jar + + + ext/juniversalchardet-1.0.3.jar + release/modules/ext/juniversalchardet-1.0.3.jar + + + ext/org.apache.felix.scr.annotations-1.6.0.jar + release/modules/ext/org.apache.felix.scr.annotations-1.6.0.jar + + + ext/noggit-0.5.jar + release/modules/ext/noggit-0.5.jar + + + ext/apache-mime4j-core-0.7.2.jar + release/modules/ext/apache-mime4j-core-0.7.2.jar + + + ext/httpmime-4.3.1.jar + release/modules/ext/httpmime-4.3.1.jar + + + ext/qdox-1.12.jar + release/modules/ext/qdox-1.12.jar + + + ext/httpcore-4.3.jar + release/modules/ext/httpcore-4.3.jar + + + ext/commons-lang-2.4.jar + release/modules/ext/commons-lang-2.4.jar + + + ext/rome-0.9.jar + release/modules/ext/rome-0.9.jar + ext/geronimo-stax-api_1.0_spec-1.0.1.jar release/modules/ext/geronimo-stax-api_1.0_spec-1.0.1.jar - ext/asm-3.1.jar - release/modules/ext/asm-3.1.jar + ext/vorbis-java-tika-0.1.jar + release/modules/ext/vorbis-java-tika-0.1.jar + + + ext/jdom-1.0.jar + release/modules/ext/jdom-1.0.jar + + + ext/poi-ooxml-schemas-3.10-beta2.jar + release/modules/ext/poi-ooxml-schemas-3.10-beta2.jar + + + ext/pdfbox-1.8.4.jar + release/modules/ext/pdfbox-1.8.4.jar + + + ext/wstx-asl-3.2.7.jar + release/modules/ext/wstx-asl-3.2.7.jar + + + ext/aspectjrt-1.6.11.jar + release/modules/ext/aspectjrt-1.6.11.jar + + + ext/dom4j-1.6.1.jar + release/modules/ext/dom4j-1.6.1.jar + + + ext/bcprov-jdk15-1.45.jar + release/modules/ext/bcprov-jdk15-1.45.jar diff --git a/KeywordSearch/release/solr/contexts/solr-jetty-context.xml b/KeywordSearch/release/solr/contexts/solr-jetty-context.xml new file mode 100644 index 0000000000..a84d325e74 --- /dev/null +++ b/KeywordSearch/release/solr/contexts/solr-jetty-context.xml @@ -0,0 +1,8 @@ + + + + + /webapps/solr.war + /etc/webdefault.xml + + diff --git a/KeywordSearch/release/solr/etc/jetty.xml b/KeywordSearch/release/solr/etc/jetty.xml index 87031f4801..4dbf5070c5 100644 --- a/KeywordSearch/release/solr/etc/jetty.xml +++ b/KeywordSearch/release/solr/etc/jetty.xml @@ -1,60 +1,40 @@ - + - + - - - - - org.mortbay.jetty.Request.maxFormContentSize - 1000000 - + - - + + 10 10000 - 20 + false - - - - - - - - - + + 50000 1500 @@ -80,142 +60,91 @@ - - - - - + - - - - - - - - - - + # Run solr example with SSL requiring client certs on port 8984 + java -Djetty.ssl.clientAuth=true -jar start.jar + # + # Run post.jar so that it trusts the server cert, + # and authenticates with a client cert + java -Djavax.net.ssl.keyStorePassword=secret -Djavax.net.ssl.keyStore=../etc/solrtest.keystore -Djavax.net.ssl.trustStore=../etc/solrtest.keystore -Durl=https://localhost:8984/solr/update -jar post.jar *.xml + + --> - + - + - + - + - + - - - - - - - - - - - - - - - - /contexts - 5 - - - - - - - - - - - - - - - - - - - - - - /webapps - false - true - false - /etc/webdefault.xml - - - - - - - - - - - - - - - - - - + --> + @@ -223,5 +152,53 @@ false false 1000 + false + false + + + + + + + + + + + + org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern + .*/servlet-api-[^/]*\.jar$ + + + + + + + + + + + + + + + /contexts + 0 + + + + diff --git a/KeywordSearch/release/solr/etc/webdefault.xml b/KeywordSearch/release/solr/etc/webdefault.xml index b8d0114169..213138b35f 100644 --- a/KeywordSearch/release/solr/etc/webdefault.xml +++ b/KeywordSearch/release/solr/etc/webdefault.xml @@ -1,118 +1,126 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + Default web.xml file. This file is applied to a Web application before it's own WEB_INF/web.xml file + + + + + + org.eclipse.jetty.servlet.listener.ELContextCleaner + + + + + + + + org.eclipse.jetty.servlet.listener.IntrospectorCleaner + + - - - org.mortbay.jetty.webapp.NoTLDJarPattern - start.jar|ant-.*\.jar|dojo-.*\.jar|jetty-.*\.jar|jsp-api-.*\.jar|junit-.*\.jar|servlet-api-.*\.jar|dnsns\.jar|rt\.jar|jsse\.jar|tools\.jar|sunpkcs11\.jar|sunjce_provider\.jar|xerces.*\.jar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + default - org.mortbay.jetty.servlet.DefaultServlet + org.eclipse.jetty.servlet.DefaultServlet + + aliases + false + acceptRanges true @@ -135,15 +143,11 @@ maxCachedFileSize - 10000000 + 200000000 maxCachedFiles - 1000 - - - cacheType - both + 2048 gzip @@ -152,7 +156,13 @@ useFileMappedBuffer true - + + 0 - + + + + default + / + - default / - @@ -250,20 +263,22 @@ - + jsp org.apache.jasper.servlet.JspServlet - logVerbosityLevel - DEBUG + logVerbosityLevel + DEBUG - fork - false + fork + false - xpoweredBy - false + xpoweredBy + false @@ -305,26 +320,11 @@ - @@ -337,7 +337,7 @@ - + - arISO-8859-6 - beISO-8859-5 - bgISO-8859-5 - caISO-8859-1 - csISO-8859-2 - daISO-8859-1 - deISO-8859-1 - elISO-8859-7 - enISO-8859-1 - esISO-8859-1 - etISO-8859-1 - fiISO-8859-1 - frISO-8859-1 - hrISO-8859-2 - huISO-8859-2 - isISO-8859-1 - itISO-8859-1 - iwISO-8859-8 - jaShift_JIS - koEUC-KR - ltISO-8859-2 - lvISO-8859-2 - mkISO-8859-5 - nlISO-8859-1 - noISO-8859-1 - plISO-8859-2 - ptISO-8859-1 - roISO-8859-2 - ruISO-8859-5 - shISO-8859-5 - skISO-8859-2 - slISO-8859-2 - sqISO-8859-2 - srISO-8859-5 - svISO-8859-1 - trISO-8859-9 - ukISO-8859-5 - zhGB2312 - zh_TWBig5 + + ar + ISO-8859-6 + + + be + ISO-8859-5 + + + bg + ISO-8859-5 + + + ca + ISO-8859-1 + + + cs + ISO-8859-2 + + + da + ISO-8859-1 + + + de + ISO-8859-1 + + + el + ISO-8859-7 + + + en + ISO-8859-1 + + + es + ISO-8859-1 + + + et + ISO-8859-1 + + + fi + ISO-8859-1 + + + fr + ISO-8859-1 + + + hr + ISO-8859-2 + + + hu + ISO-8859-2 + + + is + ISO-8859-1 + + + it + ISO-8859-1 + + + iw + ISO-8859-8 + + + ja + Shift_JIS + + + ko + EUC-KR + + + lt + ISO-8859-2 + + + lv + ISO-8859-2 + + + mk + ISO-8859-5 + + + nl + ISO-8859-1 + + + no + ISO-8859-1 + + + pl + ISO-8859-2 + + + pt + ISO-8859-1 + + + ro + ISO-8859-2 + + + ru + ISO-8859-5 + + + sh + ISO-8859-5 + + + sk + ISO-8859-2 + + + sl + ISO-8859-2 + + + sq + ISO-8859-2 + + + sr + ISO-8859-5 + + + sv + ISO-8859-1 + + + tr + ISO-8859-9 + + + uk + ISO-8859-5 + + + zh + GB2312 + + + zh_TW + Big5 + - + Disable TRACE @@ -405,6 +522,6 @@ - + diff --git a/KeywordSearch/release/solr/resources/log4j.properties b/KeywordSearch/release/solr/resources/log4j.properties new file mode 100644 index 0000000000..12a26361f5 --- /dev/null +++ b/KeywordSearch/release/solr/resources/log4j.properties @@ -0,0 +1,11 @@ +# Logging level +solr.log=logs/ +log4j.rootLogger=WARN, CONSOLE + +log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender + +log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout +log4j.appender.CONSOLE.layout.ConversionPattern=%-4r [%t] %-5p %c %x \u2013 %m%n + +# set to INFO to enable infostream log messages +log4j.logger.org.apache.solr.update.LoggingInfoStream=OFF diff --git a/KeywordSearch/release/solr/solr/conf/schema.xml b/KeywordSearch/release/solr/solr/conf/schema.xml index dddfb2f1c3..a4ecdc0e10 100644 --- a/KeywordSearch/release/solr/solr/conf/schema.xml +++ b/KeywordSearch/release/solr/solr/conf/schema.xml @@ -209,9 +209,8 @@ - - + @@ -223,8 +222,7 @@ - - + - + diff --git a/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/admin-extra.html b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/admin-extra.html new file mode 100644 index 0000000000..aa739da862 --- /dev/null +++ b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/admin-extra.html @@ -0,0 +1,31 @@ + + + diff --git a/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/elevate.xml b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/elevate.xml new file mode 100644 index 0000000000..7630ebe20f --- /dev/null +++ b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/elevate.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + diff --git a/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/logging-development.properties b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/logging-development.properties new file mode 100644 index 0000000000..34bb6c9b96 --- /dev/null +++ b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/logging-development.properties @@ -0,0 +1,4 @@ +.level = INFO + +# Write to the console, we will forward it to a file determined at runtime +handlers = java.util.logging.ConsoleHandler \ No newline at end of file diff --git a/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/logging-release.properties b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/logging-release.properties new file mode 100644 index 0000000000..1aaff5afa2 --- /dev/null +++ b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/logging-release.properties @@ -0,0 +1,4 @@ +.level = WARNING + +# Write to the console, we will forward it to a file determined at runtime +handlers = java.util.logging.ConsoleHandler \ No newline at end of file diff --git a/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/mapping-FoldToASCII.txt b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/mapping-FoldToASCII.txt new file mode 100644 index 0000000000..9a84b6eac3 --- /dev/null +++ b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/mapping-FoldToASCII.txt @@ -0,0 +1,3813 @@ +# The ASF licenses this file to You 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. + + +# This map converts alphabetic, numeric, and symbolic Unicode characters +# which are not in the first 127 ASCII characters (the "Basic Latin" Unicode +# block) into their ASCII equivalents, if one exists. +# +# Characters from the following Unicode blocks are converted; however, only +# those characters with reasonable ASCII alternatives are converted: +# +# - C1 Controls and Latin-1 Supplement: http://www.unicode.org/charts/PDF/U0080.pdf +# - Latin Extended-A: http://www.unicode.org/charts/PDF/U0100.pdf +# - Latin Extended-B: http://www.unicode.org/charts/PDF/U0180.pdf +# - Latin Extended Additional: http://www.unicode.org/charts/PDF/U1E00.pdf +# - Latin Extended-C: http://www.unicode.org/charts/PDF/U2C60.pdf +# - Latin Extended-D: http://www.unicode.org/charts/PDF/UA720.pdf +# - IPA Extensions: http://www.unicode.org/charts/PDF/U0250.pdf +# - Phonetic Extensions: http://www.unicode.org/charts/PDF/U1D00.pdf +# - Phonetic Extensions Supplement: http://www.unicode.org/charts/PDF/U1D80.pdf +# - General Punctuation: http://www.unicode.org/charts/PDF/U2000.pdf +# - Superscripts and Subscripts: http://www.unicode.org/charts/PDF/U2070.pdf +# - Enclosed Alphanumerics: http://www.unicode.org/charts/PDF/U2460.pdf +# - Dingbats: http://www.unicode.org/charts/PDF/U2700.pdf +# - Supplemental Punctuation: http://www.unicode.org/charts/PDF/U2E00.pdf +# - Alphabetic Presentation Forms: http://www.unicode.org/charts/PDF/UFB00.pdf +# - Halfwidth and Fullwidth Forms: http://www.unicode.org/charts/PDF/UFF00.pdf +# +# See: http://en.wikipedia.org/wiki/Latin_characters_in_Unicode +# +# The set of character conversions supported by this map is a superset of +# those supported by the map represented by mapping-ISOLatin1Accent.txt. +# +# See the bottom of this file for the Perl script used to generate the contents +# of this file (without this header) from ASCIIFoldingFilter.java. + + +# Syntax: +# "source" => "target" +# "source".length() > 0 (source cannot be empty.) +# "target".length() >= 0 (target can be empty.) + + +# À [LATIN CAPITAL LETTER A WITH GRAVE] +"\u00C0" => "A" + +# Á [LATIN CAPITAL LETTER A WITH ACUTE] +"\u00C1" => "A" + +#  [LATIN CAPITAL LETTER A WITH CIRCUMFLEX] +"\u00C2" => "A" + +# à [LATIN CAPITAL LETTER A WITH TILDE] +"\u00C3" => "A" + +# Ä [LATIN CAPITAL LETTER A WITH DIAERESIS] +"\u00C4" => "A" + +# Å [LATIN CAPITAL LETTER A WITH RING ABOVE] +"\u00C5" => "A" + +# Ā [LATIN CAPITAL LETTER A WITH MACRON] +"\u0100" => "A" + +# Ă [LATIN CAPITAL LETTER A WITH BREVE] +"\u0102" => "A" + +# Ą [LATIN CAPITAL LETTER A WITH OGONEK] +"\u0104" => "A" + +# Ə http://en.wikipedia.org/wiki/Schwa [LATIN CAPITAL LETTER SCHWA] +"\u018F" => "A" + +# Ǎ [LATIN CAPITAL LETTER A WITH CARON] +"\u01CD" => "A" + +# Ǟ [LATIN CAPITAL LETTER A WITH DIAERESIS AND MACRON] +"\u01DE" => "A" + +# Ǡ [LATIN CAPITAL LETTER A WITH DOT ABOVE AND MACRON] +"\u01E0" => "A" + +# Ǻ [LATIN CAPITAL LETTER A WITH RING ABOVE AND ACUTE] +"\u01FA" => "A" + +# Ȁ [LATIN CAPITAL LETTER A WITH DOUBLE GRAVE] +"\u0200" => "A" + +# Ȃ [LATIN CAPITAL LETTER A WITH INVERTED BREVE] +"\u0202" => "A" + +# Ȧ [LATIN CAPITAL LETTER A WITH DOT ABOVE] +"\u0226" => "A" + +# Ⱥ [LATIN CAPITAL LETTER A WITH STROKE] +"\u023A" => "A" + +# ᴀ [LATIN LETTER SMALL CAPITAL A] +"\u1D00" => "A" + +# Ḁ [LATIN CAPITAL LETTER A WITH RING BELOW] +"\u1E00" => "A" + +# Ạ [LATIN CAPITAL LETTER A WITH DOT BELOW] +"\u1EA0" => "A" + +# Ả [LATIN CAPITAL LETTER A WITH HOOK ABOVE] +"\u1EA2" => "A" + +# Ấ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND ACUTE] +"\u1EA4" => "A" + +# Ầ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND GRAVE] +"\u1EA6" => "A" + +# Ẩ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE] +"\u1EA8" => "A" + +# Ẫ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND TILDE] +"\u1EAA" => "A" + +# Ậ [LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND DOT BELOW] +"\u1EAC" => "A" + +# Ắ [LATIN CAPITAL LETTER A WITH BREVE AND ACUTE] +"\u1EAE" => "A" + +# Ằ [LATIN CAPITAL LETTER A WITH BREVE AND GRAVE] +"\u1EB0" => "A" + +# Ẳ [LATIN CAPITAL LETTER A WITH BREVE AND HOOK ABOVE] +"\u1EB2" => "A" + +# Ẵ [LATIN CAPITAL LETTER A WITH BREVE AND TILDE] +"\u1EB4" => "A" + +# Ặ [LATIN CAPITAL LETTER A WITH BREVE AND DOT BELOW] +"\u1EB6" => "A" + +# Ⓐ [CIRCLED LATIN CAPITAL LETTER A] +"\u24B6" => "A" + +# A [FULLWIDTH LATIN CAPITAL LETTER A] +"\uFF21" => "A" + +# à [LATIN SMALL LETTER A WITH GRAVE] +"\u00E0" => "a" + +# á [LATIN SMALL LETTER A WITH ACUTE] +"\u00E1" => "a" + +# â [LATIN SMALL LETTER A WITH CIRCUMFLEX] +"\u00E2" => "a" + +# ã [LATIN SMALL LETTER A WITH TILDE] +"\u00E3" => "a" + +# ä [LATIN SMALL LETTER A WITH DIAERESIS] +"\u00E4" => "a" + +# å [LATIN SMALL LETTER A WITH RING ABOVE] +"\u00E5" => "a" + +# ā [LATIN SMALL LETTER A WITH MACRON] +"\u0101" => "a" + +# ă [LATIN SMALL LETTER A WITH BREVE] +"\u0103" => "a" + +# ą [LATIN SMALL LETTER A WITH OGONEK] +"\u0105" => "a" + +# ǎ [LATIN SMALL LETTER A WITH CARON] +"\u01CE" => "a" + +# ǟ [LATIN SMALL LETTER A WITH DIAERESIS AND MACRON] +"\u01DF" => "a" + +# ǡ [LATIN SMALL LETTER A WITH DOT ABOVE AND MACRON] +"\u01E1" => "a" + +# ǻ [LATIN SMALL LETTER A WITH RING ABOVE AND ACUTE] +"\u01FB" => "a" + +# ȁ [LATIN SMALL LETTER A WITH DOUBLE GRAVE] +"\u0201" => "a" + +# ȃ [LATIN SMALL LETTER A WITH INVERTED BREVE] +"\u0203" => "a" + +# ȧ [LATIN SMALL LETTER A WITH DOT ABOVE] +"\u0227" => "a" + +# ɐ [LATIN SMALL LETTER TURNED A] +"\u0250" => "a" + +# ə [LATIN SMALL LETTER SCHWA] +"\u0259" => "a" + +# ɚ [LATIN SMALL LETTER SCHWA WITH HOOK] +"\u025A" => "a" + +# ᶏ [LATIN SMALL LETTER A WITH RETROFLEX HOOK] +"\u1D8F" => "a" + +# ᶕ [LATIN SMALL LETTER SCHWA WITH RETROFLEX HOOK] +"\u1D95" => "a" + +# ạ [LATIN SMALL LETTER A WITH RING BELOW] +"\u1E01" => "a" + +# ả [LATIN SMALL LETTER A WITH RIGHT HALF RING] +"\u1E9A" => "a" + +# ạ [LATIN SMALL LETTER A WITH DOT BELOW] +"\u1EA1" => "a" + +# ả [LATIN SMALL LETTER A WITH HOOK ABOVE] +"\u1EA3" => "a" + +# ấ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND ACUTE] +"\u1EA5" => "a" + +# ầ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND GRAVE] +"\u1EA7" => "a" + +# ẩ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE] +"\u1EA9" => "a" + +# ẫ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND TILDE] +"\u1EAB" => "a" + +# ậ [LATIN SMALL LETTER A WITH CIRCUMFLEX AND DOT BELOW] +"\u1EAD" => "a" + +# ắ [LATIN SMALL LETTER A WITH BREVE AND ACUTE] +"\u1EAF" => "a" + +# ằ [LATIN SMALL LETTER A WITH BREVE AND GRAVE] +"\u1EB1" => "a" + +# ẳ [LATIN SMALL LETTER A WITH BREVE AND HOOK ABOVE] +"\u1EB3" => "a" + +# ẵ [LATIN SMALL LETTER A WITH BREVE AND TILDE] +"\u1EB5" => "a" + +# ặ [LATIN SMALL LETTER A WITH BREVE AND DOT BELOW] +"\u1EB7" => "a" + +# ₐ [LATIN SUBSCRIPT SMALL LETTER A] +"\u2090" => "a" + +# ₔ [LATIN SUBSCRIPT SMALL LETTER SCHWA] +"\u2094" => "a" + +# ⓐ [CIRCLED LATIN SMALL LETTER A] +"\u24D0" => "a" + +# ⱥ [LATIN SMALL LETTER A WITH STROKE] +"\u2C65" => "a" + +# Ɐ [LATIN CAPITAL LETTER TURNED A] +"\u2C6F" => "a" + +# a [FULLWIDTH LATIN SMALL LETTER A] +"\uFF41" => "a" + +# Ꜳ [LATIN CAPITAL LETTER AA] +"\uA732" => "AA" + +# Æ [LATIN CAPITAL LETTER AE] +"\u00C6" => "AE" + +# Ǣ [LATIN CAPITAL LETTER AE WITH MACRON] +"\u01E2" => "AE" + +# Ǽ [LATIN CAPITAL LETTER AE WITH ACUTE] +"\u01FC" => "AE" + +# ᴁ [LATIN LETTER SMALL CAPITAL AE] +"\u1D01" => "AE" + +# Ꜵ [LATIN CAPITAL LETTER AO] +"\uA734" => "AO" + +# Ꜷ [LATIN CAPITAL LETTER AU] +"\uA736" => "AU" + +# Ꜹ [LATIN CAPITAL LETTER AV] +"\uA738" => "AV" + +# Ꜻ [LATIN CAPITAL LETTER AV WITH HORIZONTAL BAR] +"\uA73A" => "AV" + +# Ꜽ [LATIN CAPITAL LETTER AY] +"\uA73C" => "AY" + +# ⒜ [PARENTHESIZED LATIN SMALL LETTER A] +"\u249C" => "(a)" + +# ꜳ [LATIN SMALL LETTER AA] +"\uA733" => "aa" + +# æ [LATIN SMALL LETTER AE] +"\u00E6" => "ae" + +# ǣ [LATIN SMALL LETTER AE WITH MACRON] +"\u01E3" => "ae" + +# ǽ [LATIN SMALL LETTER AE WITH ACUTE] +"\u01FD" => "ae" + +# ᴂ [LATIN SMALL LETTER TURNED AE] +"\u1D02" => "ae" + +# ꜵ [LATIN SMALL LETTER AO] +"\uA735" => "ao" + +# ꜷ [LATIN SMALL LETTER AU] +"\uA737" => "au" + +# ꜹ [LATIN SMALL LETTER AV] +"\uA739" => "av" + +# ꜻ [LATIN SMALL LETTER AV WITH HORIZONTAL BAR] +"\uA73B" => "av" + +# ꜽ [LATIN SMALL LETTER AY] +"\uA73D" => "ay" + +# Ɓ [LATIN CAPITAL LETTER B WITH HOOK] +"\u0181" => "B" + +# Ƃ [LATIN CAPITAL LETTER B WITH TOPBAR] +"\u0182" => "B" + +# Ƀ [LATIN CAPITAL LETTER B WITH STROKE] +"\u0243" => "B" + +# ʙ [LATIN LETTER SMALL CAPITAL B] +"\u0299" => "B" + +# ᴃ [LATIN LETTER SMALL CAPITAL BARRED B] +"\u1D03" => "B" + +# Ḃ [LATIN CAPITAL LETTER B WITH DOT ABOVE] +"\u1E02" => "B" + +# Ḅ [LATIN CAPITAL LETTER B WITH DOT BELOW] +"\u1E04" => "B" + +# Ḇ [LATIN CAPITAL LETTER B WITH LINE BELOW] +"\u1E06" => "B" + +# Ⓑ [CIRCLED LATIN CAPITAL LETTER B] +"\u24B7" => "B" + +# B [FULLWIDTH LATIN CAPITAL LETTER B] +"\uFF22" => "B" + +# ƀ [LATIN SMALL LETTER B WITH STROKE] +"\u0180" => "b" + +# ƃ [LATIN SMALL LETTER B WITH TOPBAR] +"\u0183" => "b" + +# ɓ [LATIN SMALL LETTER B WITH HOOK] +"\u0253" => "b" + +# ᵬ [LATIN SMALL LETTER B WITH MIDDLE TILDE] +"\u1D6C" => "b" + +# ᶀ [LATIN SMALL LETTER B WITH PALATAL HOOK] +"\u1D80" => "b" + +# ḃ [LATIN SMALL LETTER B WITH DOT ABOVE] +"\u1E03" => "b" + +# ḅ [LATIN SMALL LETTER B WITH DOT BELOW] +"\u1E05" => "b" + +# ḇ [LATIN SMALL LETTER B WITH LINE BELOW] +"\u1E07" => "b" + +# ⓑ [CIRCLED LATIN SMALL LETTER B] +"\u24D1" => "b" + +# b [FULLWIDTH LATIN SMALL LETTER B] +"\uFF42" => "b" + +# ⒝ [PARENTHESIZED LATIN SMALL LETTER B] +"\u249D" => "(b)" + +# Ç [LATIN CAPITAL LETTER C WITH CEDILLA] +"\u00C7" => "C" + +# Ć [LATIN CAPITAL LETTER C WITH ACUTE] +"\u0106" => "C" + +# Ĉ [LATIN CAPITAL LETTER C WITH CIRCUMFLEX] +"\u0108" => "C" + +# Ċ [LATIN CAPITAL LETTER C WITH DOT ABOVE] +"\u010A" => "C" + +# Č [LATIN CAPITAL LETTER C WITH CARON] +"\u010C" => "C" + +# Ƈ [LATIN CAPITAL LETTER C WITH HOOK] +"\u0187" => "C" + +# Ȼ [LATIN CAPITAL LETTER C WITH STROKE] +"\u023B" => "C" + +# ʗ [LATIN LETTER STRETCHED C] +"\u0297" => "C" + +# ᴄ [LATIN LETTER SMALL CAPITAL C] +"\u1D04" => "C" + +# Ḉ [LATIN CAPITAL LETTER C WITH CEDILLA AND ACUTE] +"\u1E08" => "C" + +# Ⓒ [CIRCLED LATIN CAPITAL LETTER C] +"\u24B8" => "C" + +# C [FULLWIDTH LATIN CAPITAL LETTER C] +"\uFF23" => "C" + +# ç [LATIN SMALL LETTER C WITH CEDILLA] +"\u00E7" => "c" + +# ć [LATIN SMALL LETTER C WITH ACUTE] +"\u0107" => "c" + +# ĉ [LATIN SMALL LETTER C WITH CIRCUMFLEX] +"\u0109" => "c" + +# ċ [LATIN SMALL LETTER C WITH DOT ABOVE] +"\u010B" => "c" + +# č [LATIN SMALL LETTER C WITH CARON] +"\u010D" => "c" + +# ƈ [LATIN SMALL LETTER C WITH HOOK] +"\u0188" => "c" + +# ȼ [LATIN SMALL LETTER C WITH STROKE] +"\u023C" => "c" + +# ɕ [LATIN SMALL LETTER C WITH CURL] +"\u0255" => "c" + +# ḉ [LATIN SMALL LETTER C WITH CEDILLA AND ACUTE] +"\u1E09" => "c" + +# ↄ [LATIN SMALL LETTER REVERSED C] +"\u2184" => "c" + +# ⓒ [CIRCLED LATIN SMALL LETTER C] +"\u24D2" => "c" + +# Ꜿ [LATIN CAPITAL LETTER REVERSED C WITH DOT] +"\uA73E" => "c" + +# ꜿ [LATIN SMALL LETTER REVERSED C WITH DOT] +"\uA73F" => "c" + +# c [FULLWIDTH LATIN SMALL LETTER C] +"\uFF43" => "c" + +# ⒞ [PARENTHESIZED LATIN SMALL LETTER C] +"\u249E" => "(c)" + +# Ð [LATIN CAPITAL LETTER ETH] +"\u00D0" => "D" + +# Ď [LATIN CAPITAL LETTER D WITH CARON] +"\u010E" => "D" + +# Đ [LATIN CAPITAL LETTER D WITH STROKE] +"\u0110" => "D" + +# Ɖ [LATIN CAPITAL LETTER AFRICAN D] +"\u0189" => "D" + +# Ɗ [LATIN CAPITAL LETTER D WITH HOOK] +"\u018A" => "D" + +# Ƌ [LATIN CAPITAL LETTER D WITH TOPBAR] +"\u018B" => "D" + +# ᴅ [LATIN LETTER SMALL CAPITAL D] +"\u1D05" => "D" + +# ᴆ [LATIN LETTER SMALL CAPITAL ETH] +"\u1D06" => "D" + +# Ḋ [LATIN CAPITAL LETTER D WITH DOT ABOVE] +"\u1E0A" => "D" + +# Ḍ [LATIN CAPITAL LETTER D WITH DOT BELOW] +"\u1E0C" => "D" + +# Ḏ [LATIN CAPITAL LETTER D WITH LINE BELOW] +"\u1E0E" => "D" + +# Ḑ [LATIN CAPITAL LETTER D WITH CEDILLA] +"\u1E10" => "D" + +# Ḓ [LATIN CAPITAL LETTER D WITH CIRCUMFLEX BELOW] +"\u1E12" => "D" + +# Ⓓ [CIRCLED LATIN CAPITAL LETTER D] +"\u24B9" => "D" + +# Ꝺ [LATIN CAPITAL LETTER INSULAR D] +"\uA779" => "D" + +# D [FULLWIDTH LATIN CAPITAL LETTER D] +"\uFF24" => "D" + +# ð [LATIN SMALL LETTER ETH] +"\u00F0" => "d" + +# ď [LATIN SMALL LETTER D WITH CARON] +"\u010F" => "d" + +# đ [LATIN SMALL LETTER D WITH STROKE] +"\u0111" => "d" + +# ƌ [LATIN SMALL LETTER D WITH TOPBAR] +"\u018C" => "d" + +# ȡ [LATIN SMALL LETTER D WITH CURL] +"\u0221" => "d" + +# ɖ [LATIN SMALL LETTER D WITH TAIL] +"\u0256" => "d" + +# ɗ [LATIN SMALL LETTER D WITH HOOK] +"\u0257" => "d" + +# ᵭ [LATIN SMALL LETTER D WITH MIDDLE TILDE] +"\u1D6D" => "d" + +# ᶁ [LATIN SMALL LETTER D WITH PALATAL HOOK] +"\u1D81" => "d" + +# ᶑ [LATIN SMALL LETTER D WITH HOOK AND TAIL] +"\u1D91" => "d" + +# ḋ [LATIN SMALL LETTER D WITH DOT ABOVE] +"\u1E0B" => "d" + +# ḍ [LATIN SMALL LETTER D WITH DOT BELOW] +"\u1E0D" => "d" + +# ḏ [LATIN SMALL LETTER D WITH LINE BELOW] +"\u1E0F" => "d" + +# ḑ [LATIN SMALL LETTER D WITH CEDILLA] +"\u1E11" => "d" + +# ḓ [LATIN SMALL LETTER D WITH CIRCUMFLEX BELOW] +"\u1E13" => "d" + +# ⓓ [CIRCLED LATIN SMALL LETTER D] +"\u24D3" => "d" + +# ꝺ [LATIN SMALL LETTER INSULAR D] +"\uA77A" => "d" + +# d [FULLWIDTH LATIN SMALL LETTER D] +"\uFF44" => "d" + +# DŽ [LATIN CAPITAL LETTER DZ WITH CARON] +"\u01C4" => "DZ" + +# DZ [LATIN CAPITAL LETTER DZ] +"\u01F1" => "DZ" + +# Dž [LATIN CAPITAL LETTER D WITH SMALL LETTER Z WITH CARON] +"\u01C5" => "Dz" + +# Dz [LATIN CAPITAL LETTER D WITH SMALL LETTER Z] +"\u01F2" => "Dz" + +# ⒟ [PARENTHESIZED LATIN SMALL LETTER D] +"\u249F" => "(d)" + +# ȸ [LATIN SMALL LETTER DB DIGRAPH] +"\u0238" => "db" + +# dž [LATIN SMALL LETTER DZ WITH CARON] +"\u01C6" => "dz" + +# dz [LATIN SMALL LETTER DZ] +"\u01F3" => "dz" + +# ʣ [LATIN SMALL LETTER DZ DIGRAPH] +"\u02A3" => "dz" + +# ʥ [LATIN SMALL LETTER DZ DIGRAPH WITH CURL] +"\u02A5" => "dz" + +# È [LATIN CAPITAL LETTER E WITH GRAVE] +"\u00C8" => "E" + +# É [LATIN CAPITAL LETTER E WITH ACUTE] +"\u00C9" => "E" + +# Ê [LATIN CAPITAL LETTER E WITH CIRCUMFLEX] +"\u00CA" => "E" + +# Ë [LATIN CAPITAL LETTER E WITH DIAERESIS] +"\u00CB" => "E" + +# Ē [LATIN CAPITAL LETTER E WITH MACRON] +"\u0112" => "E" + +# Ĕ [LATIN CAPITAL LETTER E WITH BREVE] +"\u0114" => "E" + +# Ė [LATIN CAPITAL LETTER E WITH DOT ABOVE] +"\u0116" => "E" + +# Ę [LATIN CAPITAL LETTER E WITH OGONEK] +"\u0118" => "E" + +# Ě [LATIN CAPITAL LETTER E WITH CARON] +"\u011A" => "E" + +# Ǝ [LATIN CAPITAL LETTER REVERSED E] +"\u018E" => "E" + +# Ɛ [LATIN CAPITAL LETTER OPEN E] +"\u0190" => "E" + +# Ȅ [LATIN CAPITAL LETTER E WITH DOUBLE GRAVE] +"\u0204" => "E" + +# Ȇ [LATIN CAPITAL LETTER E WITH INVERTED BREVE] +"\u0206" => "E" + +# Ȩ [LATIN CAPITAL LETTER E WITH CEDILLA] +"\u0228" => "E" + +# Ɇ [LATIN CAPITAL LETTER E WITH STROKE] +"\u0246" => "E" + +# ᴇ [LATIN LETTER SMALL CAPITAL E] +"\u1D07" => "E" + +# Ḕ [LATIN CAPITAL LETTER E WITH MACRON AND GRAVE] +"\u1E14" => "E" + +# Ḗ [LATIN CAPITAL LETTER E WITH MACRON AND ACUTE] +"\u1E16" => "E" + +# Ḙ [LATIN CAPITAL LETTER E WITH CIRCUMFLEX BELOW] +"\u1E18" => "E" + +# Ḛ [LATIN CAPITAL LETTER E WITH TILDE BELOW] +"\u1E1A" => "E" + +# Ḝ [LATIN CAPITAL LETTER E WITH CEDILLA AND BREVE] +"\u1E1C" => "E" + +# Ẹ [LATIN CAPITAL LETTER E WITH DOT BELOW] +"\u1EB8" => "E" + +# Ẻ [LATIN CAPITAL LETTER E WITH HOOK ABOVE] +"\u1EBA" => "E" + +# Ẽ [LATIN CAPITAL LETTER E WITH TILDE] +"\u1EBC" => "E" + +# Ế [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND ACUTE] +"\u1EBE" => "E" + +# Ề [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND GRAVE] +"\u1EC0" => "E" + +# Ể [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE] +"\u1EC2" => "E" + +# Ễ [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND TILDE] +"\u1EC4" => "E" + +# Ệ [LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND DOT BELOW] +"\u1EC6" => "E" + +# Ⓔ [CIRCLED LATIN CAPITAL LETTER E] +"\u24BA" => "E" + +# ⱻ [LATIN LETTER SMALL CAPITAL TURNED E] +"\u2C7B" => "E" + +# E [FULLWIDTH LATIN CAPITAL LETTER E] +"\uFF25" => "E" + +# è [LATIN SMALL LETTER E WITH GRAVE] +"\u00E8" => "e" + +# é [LATIN SMALL LETTER E WITH ACUTE] +"\u00E9" => "e" + +# ê [LATIN SMALL LETTER E WITH CIRCUMFLEX] +"\u00EA" => "e" + +# ë [LATIN SMALL LETTER E WITH DIAERESIS] +"\u00EB" => "e" + +# ē [LATIN SMALL LETTER E WITH MACRON] +"\u0113" => "e" + +# ĕ [LATIN SMALL LETTER E WITH BREVE] +"\u0115" => "e" + +# ė [LATIN SMALL LETTER E WITH DOT ABOVE] +"\u0117" => "e" + +# ę [LATIN SMALL LETTER E WITH OGONEK] +"\u0119" => "e" + +# ě [LATIN SMALL LETTER E WITH CARON] +"\u011B" => "e" + +# ǝ [LATIN SMALL LETTER TURNED E] +"\u01DD" => "e" + +# ȅ [LATIN SMALL LETTER E WITH DOUBLE GRAVE] +"\u0205" => "e" + +# ȇ [LATIN SMALL LETTER E WITH INVERTED BREVE] +"\u0207" => "e" + +# ȩ [LATIN SMALL LETTER E WITH CEDILLA] +"\u0229" => "e" + +# ɇ [LATIN SMALL LETTER E WITH STROKE] +"\u0247" => "e" + +# ɘ [LATIN SMALL LETTER REVERSED E] +"\u0258" => "e" + +# ɛ [LATIN SMALL LETTER OPEN E] +"\u025B" => "e" + +# ɜ [LATIN SMALL LETTER REVERSED OPEN E] +"\u025C" => "e" + +# ɝ [LATIN SMALL LETTER REVERSED OPEN E WITH HOOK] +"\u025D" => "e" + +# ɞ [LATIN SMALL LETTER CLOSED REVERSED OPEN E] +"\u025E" => "e" + +# ʚ [LATIN SMALL LETTER CLOSED OPEN E] +"\u029A" => "e" + +# ᴈ [LATIN SMALL LETTER TURNED OPEN E] +"\u1D08" => "e" + +# ᶒ [LATIN SMALL LETTER E WITH RETROFLEX HOOK] +"\u1D92" => "e" + +# ᶓ [LATIN SMALL LETTER OPEN E WITH RETROFLEX HOOK] +"\u1D93" => "e" + +# ᶔ [LATIN SMALL LETTER REVERSED OPEN E WITH RETROFLEX HOOK] +"\u1D94" => "e" + +# ḕ [LATIN SMALL LETTER E WITH MACRON AND GRAVE] +"\u1E15" => "e" + +# ḗ [LATIN SMALL LETTER E WITH MACRON AND ACUTE] +"\u1E17" => "e" + +# ḙ [LATIN SMALL LETTER E WITH CIRCUMFLEX BELOW] +"\u1E19" => "e" + +# ḛ [LATIN SMALL LETTER E WITH TILDE BELOW] +"\u1E1B" => "e" + +# ḝ [LATIN SMALL LETTER E WITH CEDILLA AND BREVE] +"\u1E1D" => "e" + +# ẹ [LATIN SMALL LETTER E WITH DOT BELOW] +"\u1EB9" => "e" + +# ẻ [LATIN SMALL LETTER E WITH HOOK ABOVE] +"\u1EBB" => "e" + +# ẽ [LATIN SMALL LETTER E WITH TILDE] +"\u1EBD" => "e" + +# ế [LATIN SMALL LETTER E WITH CIRCUMFLEX AND ACUTE] +"\u1EBF" => "e" + +# ề [LATIN SMALL LETTER E WITH CIRCUMFLEX AND GRAVE] +"\u1EC1" => "e" + +# ể [LATIN SMALL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE] +"\u1EC3" => "e" + +# ễ [LATIN SMALL LETTER E WITH CIRCUMFLEX AND TILDE] +"\u1EC5" => "e" + +# ệ [LATIN SMALL LETTER E WITH CIRCUMFLEX AND DOT BELOW] +"\u1EC7" => "e" + +# ₑ [LATIN SUBSCRIPT SMALL LETTER E] +"\u2091" => "e" + +# ⓔ [CIRCLED LATIN SMALL LETTER E] +"\u24D4" => "e" + +# ⱸ [LATIN SMALL LETTER E WITH NOTCH] +"\u2C78" => "e" + +# e [FULLWIDTH LATIN SMALL LETTER E] +"\uFF45" => "e" + +# ⒠ [PARENTHESIZED LATIN SMALL LETTER E] +"\u24A0" => "(e)" + +# Ƒ [LATIN CAPITAL LETTER F WITH HOOK] +"\u0191" => "F" + +# Ḟ [LATIN CAPITAL LETTER F WITH DOT ABOVE] +"\u1E1E" => "F" + +# Ⓕ [CIRCLED LATIN CAPITAL LETTER F] +"\u24BB" => "F" + +# ꜰ [LATIN LETTER SMALL CAPITAL F] +"\uA730" => "F" + +# Ꝼ [LATIN CAPITAL LETTER INSULAR F] +"\uA77B" => "F" + +# ꟻ [LATIN EPIGRAPHIC LETTER REVERSED F] +"\uA7FB" => "F" + +# F [FULLWIDTH LATIN CAPITAL LETTER F] +"\uFF26" => "F" + +# ƒ [LATIN SMALL LETTER F WITH HOOK] +"\u0192" => "f" + +# ᵮ [LATIN SMALL LETTER F WITH MIDDLE TILDE] +"\u1D6E" => "f" + +# ᶂ [LATIN SMALL LETTER F WITH PALATAL HOOK] +"\u1D82" => "f" + +# ḟ [LATIN SMALL LETTER F WITH DOT ABOVE] +"\u1E1F" => "f" + +# ẛ [LATIN SMALL LETTER LONG S WITH DOT ABOVE] +"\u1E9B" => "f" + +# ⓕ [CIRCLED LATIN SMALL LETTER F] +"\u24D5" => "f" + +# ꝼ [LATIN SMALL LETTER INSULAR F] +"\uA77C" => "f" + +# f [FULLWIDTH LATIN SMALL LETTER F] +"\uFF46" => "f" + +# ⒡ [PARENTHESIZED LATIN SMALL LETTER F] +"\u24A1" => "(f)" + +# ff [LATIN SMALL LIGATURE FF] +"\uFB00" => "ff" + +# ffi [LATIN SMALL LIGATURE FFI] +"\uFB03" => "ffi" + +# ffl [LATIN SMALL LIGATURE FFL] +"\uFB04" => "ffl" + +# fi [LATIN SMALL LIGATURE FI] +"\uFB01" => "fi" + +# fl [LATIN SMALL LIGATURE FL] +"\uFB02" => "fl" + +# Ĝ [LATIN CAPITAL LETTER G WITH CIRCUMFLEX] +"\u011C" => "G" + +# Ğ [LATIN CAPITAL LETTER G WITH BREVE] +"\u011E" => "G" + +# Ġ [LATIN CAPITAL LETTER G WITH DOT ABOVE] +"\u0120" => "G" + +# Ģ [LATIN CAPITAL LETTER G WITH CEDILLA] +"\u0122" => "G" + +# Ɠ [LATIN CAPITAL LETTER G WITH HOOK] +"\u0193" => "G" + +# Ǥ [LATIN CAPITAL LETTER G WITH STROKE] +"\u01E4" => "G" + +# ǥ [LATIN SMALL LETTER G WITH STROKE] +"\u01E5" => "G" + +# Ǧ [LATIN CAPITAL LETTER G WITH CARON] +"\u01E6" => "G" + +# ǧ [LATIN SMALL LETTER G WITH CARON] +"\u01E7" => "G" + +# Ǵ [LATIN CAPITAL LETTER G WITH ACUTE] +"\u01F4" => "G" + +# ɢ [LATIN LETTER SMALL CAPITAL G] +"\u0262" => "G" + +# ʛ [LATIN LETTER SMALL CAPITAL G WITH HOOK] +"\u029B" => "G" + +# Ḡ [LATIN CAPITAL LETTER G WITH MACRON] +"\u1E20" => "G" + +# Ⓖ [CIRCLED LATIN CAPITAL LETTER G] +"\u24BC" => "G" + +# Ᵹ [LATIN CAPITAL LETTER INSULAR G] +"\uA77D" => "G" + +# Ꝿ [LATIN CAPITAL LETTER TURNED INSULAR G] +"\uA77E" => "G" + +# G [FULLWIDTH LATIN CAPITAL LETTER G] +"\uFF27" => "G" + +# ĝ [LATIN SMALL LETTER G WITH CIRCUMFLEX] +"\u011D" => "g" + +# ğ [LATIN SMALL LETTER G WITH BREVE] +"\u011F" => "g" + +# ġ [LATIN SMALL LETTER G WITH DOT ABOVE] +"\u0121" => "g" + +# ģ [LATIN SMALL LETTER G WITH CEDILLA] +"\u0123" => "g" + +# ǵ [LATIN SMALL LETTER G WITH ACUTE] +"\u01F5" => "g" + +# ɠ [LATIN SMALL LETTER G WITH HOOK] +"\u0260" => "g" + +# ɡ [LATIN SMALL LETTER SCRIPT G] +"\u0261" => "g" + +# ᵷ [LATIN SMALL LETTER TURNED G] +"\u1D77" => "g" + +# ᵹ [LATIN SMALL LETTER INSULAR G] +"\u1D79" => "g" + +# ᶃ [LATIN SMALL LETTER G WITH PALATAL HOOK] +"\u1D83" => "g" + +# ḡ [LATIN SMALL LETTER G WITH MACRON] +"\u1E21" => "g" + +# ⓖ [CIRCLED LATIN SMALL LETTER G] +"\u24D6" => "g" + +# ꝿ [LATIN SMALL LETTER TURNED INSULAR G] +"\uA77F" => "g" + +# g [FULLWIDTH LATIN SMALL LETTER G] +"\uFF47" => "g" + +# ⒢ [PARENTHESIZED LATIN SMALL LETTER G] +"\u24A2" => "(g)" + +# Ĥ [LATIN CAPITAL LETTER H WITH CIRCUMFLEX] +"\u0124" => "H" + +# Ħ [LATIN CAPITAL LETTER H WITH STROKE] +"\u0126" => "H" + +# Ȟ [LATIN CAPITAL LETTER H WITH CARON] +"\u021E" => "H" + +# ʜ [LATIN LETTER SMALL CAPITAL H] +"\u029C" => "H" + +# Ḣ [LATIN CAPITAL LETTER H WITH DOT ABOVE] +"\u1E22" => "H" + +# Ḥ [LATIN CAPITAL LETTER H WITH DOT BELOW] +"\u1E24" => "H" + +# Ḧ [LATIN CAPITAL LETTER H WITH DIAERESIS] +"\u1E26" => "H" + +# Ḩ [LATIN CAPITAL LETTER H WITH CEDILLA] +"\u1E28" => "H" + +# Ḫ [LATIN CAPITAL LETTER H WITH BREVE BELOW] +"\u1E2A" => "H" + +# Ⓗ [CIRCLED LATIN CAPITAL LETTER H] +"\u24BD" => "H" + +# Ⱨ [LATIN CAPITAL LETTER H WITH DESCENDER] +"\u2C67" => "H" + +# Ⱶ [LATIN CAPITAL LETTER HALF H] +"\u2C75" => "H" + +# H [FULLWIDTH LATIN CAPITAL LETTER H] +"\uFF28" => "H" + +# ĥ [LATIN SMALL LETTER H WITH CIRCUMFLEX] +"\u0125" => "h" + +# ħ [LATIN SMALL LETTER H WITH STROKE] +"\u0127" => "h" + +# ȟ [LATIN SMALL LETTER H WITH CARON] +"\u021F" => "h" + +# ɥ [LATIN SMALL LETTER TURNED H] +"\u0265" => "h" + +# ɦ [LATIN SMALL LETTER H WITH HOOK] +"\u0266" => "h" + +# ʮ [LATIN SMALL LETTER TURNED H WITH FISHHOOK] +"\u02AE" => "h" + +# ʯ [LATIN SMALL LETTER TURNED H WITH FISHHOOK AND TAIL] +"\u02AF" => "h" + +# ḣ [LATIN SMALL LETTER H WITH DOT ABOVE] +"\u1E23" => "h" + +# ḥ [LATIN SMALL LETTER H WITH DOT BELOW] +"\u1E25" => "h" + +# ḧ [LATIN SMALL LETTER H WITH DIAERESIS] +"\u1E27" => "h" + +# ḩ [LATIN SMALL LETTER H WITH CEDILLA] +"\u1E29" => "h" + +# ḫ [LATIN SMALL LETTER H WITH BREVE BELOW] +"\u1E2B" => "h" + +# ẖ [LATIN SMALL LETTER H WITH LINE BELOW] +"\u1E96" => "h" + +# ⓗ [CIRCLED LATIN SMALL LETTER H] +"\u24D7" => "h" + +# ⱨ [LATIN SMALL LETTER H WITH DESCENDER] +"\u2C68" => "h" + +# ⱶ [LATIN SMALL LETTER HALF H] +"\u2C76" => "h" + +# h [FULLWIDTH LATIN SMALL LETTER H] +"\uFF48" => "h" + +# Ƕ http://en.wikipedia.org/wiki/Hwair [LATIN CAPITAL LETTER HWAIR] +"\u01F6" => "HV" + +# ⒣ [PARENTHESIZED LATIN SMALL LETTER H] +"\u24A3" => "(h)" + +# ƕ [LATIN SMALL LETTER HV] +"\u0195" => "hv" + +# Ì [LATIN CAPITAL LETTER I WITH GRAVE] +"\u00CC" => "I" + +# Í [LATIN CAPITAL LETTER I WITH ACUTE] +"\u00CD" => "I" + +# Î [LATIN CAPITAL LETTER I WITH CIRCUMFLEX] +"\u00CE" => "I" + +# Ï [LATIN CAPITAL LETTER I WITH DIAERESIS] +"\u00CF" => "I" + +# Ĩ [LATIN CAPITAL LETTER I WITH TILDE] +"\u0128" => "I" + +# Ī [LATIN CAPITAL LETTER I WITH MACRON] +"\u012A" => "I" + +# Ĭ [LATIN CAPITAL LETTER I WITH BREVE] +"\u012C" => "I" + +# Į [LATIN CAPITAL LETTER I WITH OGONEK] +"\u012E" => "I" + +# İ [LATIN CAPITAL LETTER I WITH DOT ABOVE] +"\u0130" => "I" + +# Ɩ [LATIN CAPITAL LETTER IOTA] +"\u0196" => "I" + +# Ɨ [LATIN CAPITAL LETTER I WITH STROKE] +"\u0197" => "I" + +# Ǐ [LATIN CAPITAL LETTER I WITH CARON] +"\u01CF" => "I" + +# Ȉ [LATIN CAPITAL LETTER I WITH DOUBLE GRAVE] +"\u0208" => "I" + +# Ȋ [LATIN CAPITAL LETTER I WITH INVERTED BREVE] +"\u020A" => "I" + +# ɪ [LATIN LETTER SMALL CAPITAL I] +"\u026A" => "I" + +# ᵻ [LATIN SMALL CAPITAL LETTER I WITH STROKE] +"\u1D7B" => "I" + +# Ḭ [LATIN CAPITAL LETTER I WITH TILDE BELOW] +"\u1E2C" => "I" + +# Ḯ [LATIN CAPITAL LETTER I WITH DIAERESIS AND ACUTE] +"\u1E2E" => "I" + +# Ỉ [LATIN CAPITAL LETTER I WITH HOOK ABOVE] +"\u1EC8" => "I" + +# Ị [LATIN CAPITAL LETTER I WITH DOT BELOW] +"\u1ECA" => "I" + +# Ⓘ [CIRCLED LATIN CAPITAL LETTER I] +"\u24BE" => "I" + +# ꟾ [LATIN EPIGRAPHIC LETTER I LONGA] +"\uA7FE" => "I" + +# I [FULLWIDTH LATIN CAPITAL LETTER I] +"\uFF29" => "I" + +# ì [LATIN SMALL LETTER I WITH GRAVE] +"\u00EC" => "i" + +# í [LATIN SMALL LETTER I WITH ACUTE] +"\u00ED" => "i" + +# î [LATIN SMALL LETTER I WITH CIRCUMFLEX] +"\u00EE" => "i" + +# ï [LATIN SMALL LETTER I WITH DIAERESIS] +"\u00EF" => "i" + +# ĩ [LATIN SMALL LETTER I WITH TILDE] +"\u0129" => "i" + +# ī [LATIN SMALL LETTER I WITH MACRON] +"\u012B" => "i" + +# ĭ [LATIN SMALL LETTER I WITH BREVE] +"\u012D" => "i" + +# į [LATIN SMALL LETTER I WITH OGONEK] +"\u012F" => "i" + +# ı [LATIN SMALL LETTER DOTLESS I] +"\u0131" => "i" + +# ǐ [LATIN SMALL LETTER I WITH CARON] +"\u01D0" => "i" + +# ȉ [LATIN SMALL LETTER I WITH DOUBLE GRAVE] +"\u0209" => "i" + +# ȋ [LATIN SMALL LETTER I WITH INVERTED BREVE] +"\u020B" => "i" + +# ɨ [LATIN SMALL LETTER I WITH STROKE] +"\u0268" => "i" + +# ᴉ [LATIN SMALL LETTER TURNED I] +"\u1D09" => "i" + +# ᵢ [LATIN SUBSCRIPT SMALL LETTER I] +"\u1D62" => "i" + +# ᵼ [LATIN SMALL LETTER IOTA WITH STROKE] +"\u1D7C" => "i" + +# ᶖ [LATIN SMALL LETTER I WITH RETROFLEX HOOK] +"\u1D96" => "i" + +# ḭ [LATIN SMALL LETTER I WITH TILDE BELOW] +"\u1E2D" => "i" + +# ḯ [LATIN SMALL LETTER I WITH DIAERESIS AND ACUTE] +"\u1E2F" => "i" + +# ỉ [LATIN SMALL LETTER I WITH HOOK ABOVE] +"\u1EC9" => "i" + +# ị [LATIN SMALL LETTER I WITH DOT BELOW] +"\u1ECB" => "i" + +# ⁱ [SUPERSCRIPT LATIN SMALL LETTER I] +"\u2071" => "i" + +# ⓘ [CIRCLED LATIN SMALL LETTER I] +"\u24D8" => "i" + +# i [FULLWIDTH LATIN SMALL LETTER I] +"\uFF49" => "i" + +# IJ [LATIN CAPITAL LIGATURE IJ] +"\u0132" => "IJ" + +# ⒤ [PARENTHESIZED LATIN SMALL LETTER I] +"\u24A4" => "(i)" + +# ij [LATIN SMALL LIGATURE IJ] +"\u0133" => "ij" + +# Ĵ [LATIN CAPITAL LETTER J WITH CIRCUMFLEX] +"\u0134" => "J" + +# Ɉ [LATIN CAPITAL LETTER J WITH STROKE] +"\u0248" => "J" + +# ᴊ [LATIN LETTER SMALL CAPITAL J] +"\u1D0A" => "J" + +# Ⓙ [CIRCLED LATIN CAPITAL LETTER J] +"\u24BF" => "J" + +# J [FULLWIDTH LATIN CAPITAL LETTER J] +"\uFF2A" => "J" + +# ĵ [LATIN SMALL LETTER J WITH CIRCUMFLEX] +"\u0135" => "j" + +# ǰ [LATIN SMALL LETTER J WITH CARON] +"\u01F0" => "j" + +# ȷ [LATIN SMALL LETTER DOTLESS J] +"\u0237" => "j" + +# ɉ [LATIN SMALL LETTER J WITH STROKE] +"\u0249" => "j" + +# ɟ [LATIN SMALL LETTER DOTLESS J WITH STROKE] +"\u025F" => "j" + +# ʄ [LATIN SMALL LETTER DOTLESS J WITH STROKE AND HOOK] +"\u0284" => "j" + +# ʝ [LATIN SMALL LETTER J WITH CROSSED-TAIL] +"\u029D" => "j" + +# ⓙ [CIRCLED LATIN SMALL LETTER J] +"\u24D9" => "j" + +# ⱼ [LATIN SUBSCRIPT SMALL LETTER J] +"\u2C7C" => "j" + +# j [FULLWIDTH LATIN SMALL LETTER J] +"\uFF4A" => "j" + +# ⒥ [PARENTHESIZED LATIN SMALL LETTER J] +"\u24A5" => "(j)" + +# Ķ [LATIN CAPITAL LETTER K WITH CEDILLA] +"\u0136" => "K" + +# Ƙ [LATIN CAPITAL LETTER K WITH HOOK] +"\u0198" => "K" + +# Ǩ [LATIN CAPITAL LETTER K WITH CARON] +"\u01E8" => "K" + +# ᴋ [LATIN LETTER SMALL CAPITAL K] +"\u1D0B" => "K" + +# Ḱ [LATIN CAPITAL LETTER K WITH ACUTE] +"\u1E30" => "K" + +# Ḳ [LATIN CAPITAL LETTER K WITH DOT BELOW] +"\u1E32" => "K" + +# Ḵ [LATIN CAPITAL LETTER K WITH LINE BELOW] +"\u1E34" => "K" + +# Ⓚ [CIRCLED LATIN CAPITAL LETTER K] +"\u24C0" => "K" + +# Ⱪ [LATIN CAPITAL LETTER K WITH DESCENDER] +"\u2C69" => "K" + +# Ꝁ [LATIN CAPITAL LETTER K WITH STROKE] +"\uA740" => "K" + +# Ꝃ [LATIN CAPITAL LETTER K WITH DIAGONAL STROKE] +"\uA742" => "K" + +# Ꝅ [LATIN CAPITAL LETTER K WITH STROKE AND DIAGONAL STROKE] +"\uA744" => "K" + +# K [FULLWIDTH LATIN CAPITAL LETTER K] +"\uFF2B" => "K" + +# ķ [LATIN SMALL LETTER K WITH CEDILLA] +"\u0137" => "k" + +# ƙ [LATIN SMALL LETTER K WITH HOOK] +"\u0199" => "k" + +# ǩ [LATIN SMALL LETTER K WITH CARON] +"\u01E9" => "k" + +# ʞ [LATIN SMALL LETTER TURNED K] +"\u029E" => "k" + +# ᶄ [LATIN SMALL LETTER K WITH PALATAL HOOK] +"\u1D84" => "k" + +# ḱ [LATIN SMALL LETTER K WITH ACUTE] +"\u1E31" => "k" + +# ḳ [LATIN SMALL LETTER K WITH DOT BELOW] +"\u1E33" => "k" + +# ḵ [LATIN SMALL LETTER K WITH LINE BELOW] +"\u1E35" => "k" + +# ⓚ [CIRCLED LATIN SMALL LETTER K] +"\u24DA" => "k" + +# ⱪ [LATIN SMALL LETTER K WITH DESCENDER] +"\u2C6A" => "k" + +# ꝁ [LATIN SMALL LETTER K WITH STROKE] +"\uA741" => "k" + +# ꝃ [LATIN SMALL LETTER K WITH DIAGONAL STROKE] +"\uA743" => "k" + +# ꝅ [LATIN SMALL LETTER K WITH STROKE AND DIAGONAL STROKE] +"\uA745" => "k" + +# k [FULLWIDTH LATIN SMALL LETTER K] +"\uFF4B" => "k" + +# ⒦ [PARENTHESIZED LATIN SMALL LETTER K] +"\u24A6" => "(k)" + +# Ĺ [LATIN CAPITAL LETTER L WITH ACUTE] +"\u0139" => "L" + +# Ļ [LATIN CAPITAL LETTER L WITH CEDILLA] +"\u013B" => "L" + +# Ľ [LATIN CAPITAL LETTER L WITH CARON] +"\u013D" => "L" + +# Ŀ [LATIN CAPITAL LETTER L WITH MIDDLE DOT] +"\u013F" => "L" + +# Ł [LATIN CAPITAL LETTER L WITH STROKE] +"\u0141" => "L" + +# Ƚ [LATIN CAPITAL LETTER L WITH BAR] +"\u023D" => "L" + +# ʟ [LATIN LETTER SMALL CAPITAL L] +"\u029F" => "L" + +# ᴌ [LATIN LETTER SMALL CAPITAL L WITH STROKE] +"\u1D0C" => "L" + +# Ḷ [LATIN CAPITAL LETTER L WITH DOT BELOW] +"\u1E36" => "L" + +# Ḹ [LATIN CAPITAL LETTER L WITH DOT BELOW AND MACRON] +"\u1E38" => "L" + +# Ḻ [LATIN CAPITAL LETTER L WITH LINE BELOW] +"\u1E3A" => "L" + +# Ḽ [LATIN CAPITAL LETTER L WITH CIRCUMFLEX BELOW] +"\u1E3C" => "L" + +# Ⓛ [CIRCLED LATIN CAPITAL LETTER L] +"\u24C1" => "L" + +# Ⱡ [LATIN CAPITAL LETTER L WITH DOUBLE BAR] +"\u2C60" => "L" + +# Ɫ [LATIN CAPITAL LETTER L WITH MIDDLE TILDE] +"\u2C62" => "L" + +# Ꝇ [LATIN CAPITAL LETTER BROKEN L] +"\uA746" => "L" + +# Ꝉ [LATIN CAPITAL LETTER L WITH HIGH STROKE] +"\uA748" => "L" + +# Ꞁ [LATIN CAPITAL LETTER TURNED L] +"\uA780" => "L" + +# L [FULLWIDTH LATIN CAPITAL LETTER L] +"\uFF2C" => "L" + +# ĺ [LATIN SMALL LETTER L WITH ACUTE] +"\u013A" => "l" + +# ļ [LATIN SMALL LETTER L WITH CEDILLA] +"\u013C" => "l" + +# ľ [LATIN SMALL LETTER L WITH CARON] +"\u013E" => "l" + +# ŀ [LATIN SMALL LETTER L WITH MIDDLE DOT] +"\u0140" => "l" + +# ł [LATIN SMALL LETTER L WITH STROKE] +"\u0142" => "l" + +# ƚ [LATIN SMALL LETTER L WITH BAR] +"\u019A" => "l" + +# ȴ [LATIN SMALL LETTER L WITH CURL] +"\u0234" => "l" + +# ɫ [LATIN SMALL LETTER L WITH MIDDLE TILDE] +"\u026B" => "l" + +# ɬ [LATIN SMALL LETTER L WITH BELT] +"\u026C" => "l" + +# ɭ [LATIN SMALL LETTER L WITH RETROFLEX HOOK] +"\u026D" => "l" + +# ᶅ [LATIN SMALL LETTER L WITH PALATAL HOOK] +"\u1D85" => "l" + +# ḷ [LATIN SMALL LETTER L WITH DOT BELOW] +"\u1E37" => "l" + +# ḹ [LATIN SMALL LETTER L WITH DOT BELOW AND MACRON] +"\u1E39" => "l" + +# ḻ [LATIN SMALL LETTER L WITH LINE BELOW] +"\u1E3B" => "l" + +# ḽ [LATIN SMALL LETTER L WITH CIRCUMFLEX BELOW] +"\u1E3D" => "l" + +# ⓛ [CIRCLED LATIN SMALL LETTER L] +"\u24DB" => "l" + +# ⱡ [LATIN SMALL LETTER L WITH DOUBLE BAR] +"\u2C61" => "l" + +# ꝇ [LATIN SMALL LETTER BROKEN L] +"\uA747" => "l" + +# ꝉ [LATIN SMALL LETTER L WITH HIGH STROKE] +"\uA749" => "l" + +# ꞁ [LATIN SMALL LETTER TURNED L] +"\uA781" => "l" + +# l [FULLWIDTH LATIN SMALL LETTER L] +"\uFF4C" => "l" + +# LJ [LATIN CAPITAL LETTER LJ] +"\u01C7" => "LJ" + +# Ỻ [LATIN CAPITAL LETTER MIDDLE-WELSH LL] +"\u1EFA" => "LL" + +# Lj [LATIN CAPITAL LETTER L WITH SMALL LETTER J] +"\u01C8" => "Lj" + +# ⒧ [PARENTHESIZED LATIN SMALL LETTER L] +"\u24A7" => "(l)" + +# lj [LATIN SMALL LETTER LJ] +"\u01C9" => "lj" + +# ỻ [LATIN SMALL LETTER MIDDLE-WELSH LL] +"\u1EFB" => "ll" + +# ʪ [LATIN SMALL LETTER LS DIGRAPH] +"\u02AA" => "ls" + +# ʫ [LATIN SMALL LETTER LZ DIGRAPH] +"\u02AB" => "lz" + +# Ɯ [LATIN CAPITAL LETTER TURNED M] +"\u019C" => "M" + +# ᴍ [LATIN LETTER SMALL CAPITAL M] +"\u1D0D" => "M" + +# Ḿ [LATIN CAPITAL LETTER M WITH ACUTE] +"\u1E3E" => "M" + +# Ṁ [LATIN CAPITAL LETTER M WITH DOT ABOVE] +"\u1E40" => "M" + +# Ṃ [LATIN CAPITAL LETTER M WITH DOT BELOW] +"\u1E42" => "M" + +# Ⓜ [CIRCLED LATIN CAPITAL LETTER M] +"\u24C2" => "M" + +# Ɱ [LATIN CAPITAL LETTER M WITH HOOK] +"\u2C6E" => "M" + +# ꟽ [LATIN EPIGRAPHIC LETTER INVERTED M] +"\uA7FD" => "M" + +# ꟿ [LATIN EPIGRAPHIC LETTER ARCHAIC M] +"\uA7FF" => "M" + +# M [FULLWIDTH LATIN CAPITAL LETTER M] +"\uFF2D" => "M" + +# ɯ [LATIN SMALL LETTER TURNED M] +"\u026F" => "m" + +# ɰ [LATIN SMALL LETTER TURNED M WITH LONG LEG] +"\u0270" => "m" + +# ɱ [LATIN SMALL LETTER M WITH HOOK] +"\u0271" => "m" + +# ᵯ [LATIN SMALL LETTER M WITH MIDDLE TILDE] +"\u1D6F" => "m" + +# ᶆ [LATIN SMALL LETTER M WITH PALATAL HOOK] +"\u1D86" => "m" + +# ḿ [LATIN SMALL LETTER M WITH ACUTE] +"\u1E3F" => "m" + +# ṁ [LATIN SMALL LETTER M WITH DOT ABOVE] +"\u1E41" => "m" + +# ṃ [LATIN SMALL LETTER M WITH DOT BELOW] +"\u1E43" => "m" + +# ⓜ [CIRCLED LATIN SMALL LETTER M] +"\u24DC" => "m" + +# m [FULLWIDTH LATIN SMALL LETTER M] +"\uFF4D" => "m" + +# ⒨ [PARENTHESIZED LATIN SMALL LETTER M] +"\u24A8" => "(m)" + +# Ñ [LATIN CAPITAL LETTER N WITH TILDE] +"\u00D1" => "N" + +# Ń [LATIN CAPITAL LETTER N WITH ACUTE] +"\u0143" => "N" + +# Ņ [LATIN CAPITAL LETTER N WITH CEDILLA] +"\u0145" => "N" + +# Ň [LATIN CAPITAL LETTER N WITH CARON] +"\u0147" => "N" + +# Ŋ http://en.wikipedia.org/wiki/Eng_(letter) [LATIN CAPITAL LETTER ENG] +"\u014A" => "N" + +# Ɲ [LATIN CAPITAL LETTER N WITH LEFT HOOK] +"\u019D" => "N" + +# Ǹ [LATIN CAPITAL LETTER N WITH GRAVE] +"\u01F8" => "N" + +# Ƞ [LATIN CAPITAL LETTER N WITH LONG RIGHT LEG] +"\u0220" => "N" + +# ɴ [LATIN LETTER SMALL CAPITAL N] +"\u0274" => "N" + +# ᴎ [LATIN LETTER SMALL CAPITAL REVERSED N] +"\u1D0E" => "N" + +# Ṅ [LATIN CAPITAL LETTER N WITH DOT ABOVE] +"\u1E44" => "N" + +# Ṇ [LATIN CAPITAL LETTER N WITH DOT BELOW] +"\u1E46" => "N" + +# Ṉ [LATIN CAPITAL LETTER N WITH LINE BELOW] +"\u1E48" => "N" + +# Ṋ [LATIN CAPITAL LETTER N WITH CIRCUMFLEX BELOW] +"\u1E4A" => "N" + +# Ⓝ [CIRCLED LATIN CAPITAL LETTER N] +"\u24C3" => "N" + +# N [FULLWIDTH LATIN CAPITAL LETTER N] +"\uFF2E" => "N" + +# ñ [LATIN SMALL LETTER N WITH TILDE] +"\u00F1" => "n" + +# ń [LATIN SMALL LETTER N WITH ACUTE] +"\u0144" => "n" + +# ņ [LATIN SMALL LETTER N WITH CEDILLA] +"\u0146" => "n" + +# ň [LATIN SMALL LETTER N WITH CARON] +"\u0148" => "n" + +# ʼn [LATIN SMALL LETTER N PRECEDED BY APOSTROPHE] +"\u0149" => "n" + +# ŋ http://en.wikipedia.org/wiki/Eng_(letter) [LATIN SMALL LETTER ENG] +"\u014B" => "n" + +# ƞ [LATIN SMALL LETTER N WITH LONG RIGHT LEG] +"\u019E" => "n" + +# ǹ [LATIN SMALL LETTER N WITH GRAVE] +"\u01F9" => "n" + +# ȵ [LATIN SMALL LETTER N WITH CURL] +"\u0235" => "n" + +# ɲ [LATIN SMALL LETTER N WITH LEFT HOOK] +"\u0272" => "n" + +# ɳ [LATIN SMALL LETTER N WITH RETROFLEX HOOK] +"\u0273" => "n" + +# ᵰ [LATIN SMALL LETTER N WITH MIDDLE TILDE] +"\u1D70" => "n" + +# ᶇ [LATIN SMALL LETTER N WITH PALATAL HOOK] +"\u1D87" => "n" + +# ṅ [LATIN SMALL LETTER N WITH DOT ABOVE] +"\u1E45" => "n" + +# ṇ [LATIN SMALL LETTER N WITH DOT BELOW] +"\u1E47" => "n" + +# ṉ [LATIN SMALL LETTER N WITH LINE BELOW] +"\u1E49" => "n" + +# ṋ [LATIN SMALL LETTER N WITH CIRCUMFLEX BELOW] +"\u1E4B" => "n" + +# ⁿ [SUPERSCRIPT LATIN SMALL LETTER N] +"\u207F" => "n" + +# ⓝ [CIRCLED LATIN SMALL LETTER N] +"\u24DD" => "n" + +# n [FULLWIDTH LATIN SMALL LETTER N] +"\uFF4E" => "n" + +# NJ [LATIN CAPITAL LETTER NJ] +"\u01CA" => "NJ" + +# Nj [LATIN CAPITAL LETTER N WITH SMALL LETTER J] +"\u01CB" => "Nj" + +# ⒩ [PARENTHESIZED LATIN SMALL LETTER N] +"\u24A9" => "(n)" + +# nj [LATIN SMALL LETTER NJ] +"\u01CC" => "nj" + +# Ò [LATIN CAPITAL LETTER O WITH GRAVE] +"\u00D2" => "O" + +# Ó [LATIN CAPITAL LETTER O WITH ACUTE] +"\u00D3" => "O" + +# Ô [LATIN CAPITAL LETTER O WITH CIRCUMFLEX] +"\u00D4" => "O" + +# Õ [LATIN CAPITAL LETTER O WITH TILDE] +"\u00D5" => "O" + +# Ö [LATIN CAPITAL LETTER O WITH DIAERESIS] +"\u00D6" => "O" + +# Ø [LATIN CAPITAL LETTER O WITH STROKE] +"\u00D8" => "O" + +# Ō [LATIN CAPITAL LETTER O WITH MACRON] +"\u014C" => "O" + +# Ŏ [LATIN CAPITAL LETTER O WITH BREVE] +"\u014E" => "O" + +# Ő [LATIN CAPITAL LETTER O WITH DOUBLE ACUTE] +"\u0150" => "O" + +# Ɔ [LATIN CAPITAL LETTER OPEN O] +"\u0186" => "O" + +# Ɵ [LATIN CAPITAL LETTER O WITH MIDDLE TILDE] +"\u019F" => "O" + +# Ơ [LATIN CAPITAL LETTER O WITH HORN] +"\u01A0" => "O" + +# Ǒ [LATIN CAPITAL LETTER O WITH CARON] +"\u01D1" => "O" + +# Ǫ [LATIN CAPITAL LETTER O WITH OGONEK] +"\u01EA" => "O" + +# Ǭ [LATIN CAPITAL LETTER O WITH OGONEK AND MACRON] +"\u01EC" => "O" + +# Ǿ [LATIN CAPITAL LETTER O WITH STROKE AND ACUTE] +"\u01FE" => "O" + +# Ȍ [LATIN CAPITAL LETTER O WITH DOUBLE GRAVE] +"\u020C" => "O" + +# Ȏ [LATIN CAPITAL LETTER O WITH INVERTED BREVE] +"\u020E" => "O" + +# Ȫ [LATIN CAPITAL LETTER O WITH DIAERESIS AND MACRON] +"\u022A" => "O" + +# Ȭ [LATIN CAPITAL LETTER O WITH TILDE AND MACRON] +"\u022C" => "O" + +# Ȯ [LATIN CAPITAL LETTER O WITH DOT ABOVE] +"\u022E" => "O" + +# Ȱ [LATIN CAPITAL LETTER O WITH DOT ABOVE AND MACRON] +"\u0230" => "O" + +# ᴏ [LATIN LETTER SMALL CAPITAL O] +"\u1D0F" => "O" + +# ᴐ [LATIN LETTER SMALL CAPITAL OPEN O] +"\u1D10" => "O" + +# Ṍ [LATIN CAPITAL LETTER O WITH TILDE AND ACUTE] +"\u1E4C" => "O" + +# Ṏ [LATIN CAPITAL LETTER O WITH TILDE AND DIAERESIS] +"\u1E4E" => "O" + +# Ṑ [LATIN CAPITAL LETTER O WITH MACRON AND GRAVE] +"\u1E50" => "O" + +# Ṓ [LATIN CAPITAL LETTER O WITH MACRON AND ACUTE] +"\u1E52" => "O" + +# Ọ [LATIN CAPITAL LETTER O WITH DOT BELOW] +"\u1ECC" => "O" + +# Ỏ [LATIN CAPITAL LETTER O WITH HOOK ABOVE] +"\u1ECE" => "O" + +# Ố [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND ACUTE] +"\u1ED0" => "O" + +# Ồ [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND GRAVE] +"\u1ED2" => "O" + +# Ổ [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE] +"\u1ED4" => "O" + +# Ỗ [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND TILDE] +"\u1ED6" => "O" + +# Ộ [LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND DOT BELOW] +"\u1ED8" => "O" + +# Ớ [LATIN CAPITAL LETTER O WITH HORN AND ACUTE] +"\u1EDA" => "O" + +# Ờ [LATIN CAPITAL LETTER O WITH HORN AND GRAVE] +"\u1EDC" => "O" + +# Ở [LATIN CAPITAL LETTER O WITH HORN AND HOOK ABOVE] +"\u1EDE" => "O" + +# Ỡ [LATIN CAPITAL LETTER O WITH HORN AND TILDE] +"\u1EE0" => "O" + +# Ợ [LATIN CAPITAL LETTER O WITH HORN AND DOT BELOW] +"\u1EE2" => "O" + +# Ⓞ [CIRCLED LATIN CAPITAL LETTER O] +"\u24C4" => "O" + +# Ꝋ [LATIN CAPITAL LETTER O WITH LONG STROKE OVERLAY] +"\uA74A" => "O" + +# Ꝍ [LATIN CAPITAL LETTER O WITH LOOP] +"\uA74C" => "O" + +# O [FULLWIDTH LATIN CAPITAL LETTER O] +"\uFF2F" => "O" + +# ò [LATIN SMALL LETTER O WITH GRAVE] +"\u00F2" => "o" + +# ó [LATIN SMALL LETTER O WITH ACUTE] +"\u00F3" => "o" + +# ô [LATIN SMALL LETTER O WITH CIRCUMFLEX] +"\u00F4" => "o" + +# õ [LATIN SMALL LETTER O WITH TILDE] +"\u00F5" => "o" + +# ö [LATIN SMALL LETTER O WITH DIAERESIS] +"\u00F6" => "o" + +# ø [LATIN SMALL LETTER O WITH STROKE] +"\u00F8" => "o" + +# ō [LATIN SMALL LETTER O WITH MACRON] +"\u014D" => "o" + +# ŏ [LATIN SMALL LETTER O WITH BREVE] +"\u014F" => "o" + +# ő [LATIN SMALL LETTER O WITH DOUBLE ACUTE] +"\u0151" => "o" + +# ơ [LATIN SMALL LETTER O WITH HORN] +"\u01A1" => "o" + +# ǒ [LATIN SMALL LETTER O WITH CARON] +"\u01D2" => "o" + +# ǫ [LATIN SMALL LETTER O WITH OGONEK] +"\u01EB" => "o" + +# ǭ [LATIN SMALL LETTER O WITH OGONEK AND MACRON] +"\u01ED" => "o" + +# ǿ [LATIN SMALL LETTER O WITH STROKE AND ACUTE] +"\u01FF" => "o" + +# ȍ [LATIN SMALL LETTER O WITH DOUBLE GRAVE] +"\u020D" => "o" + +# ȏ [LATIN SMALL LETTER O WITH INVERTED BREVE] +"\u020F" => "o" + +# ȫ [LATIN SMALL LETTER O WITH DIAERESIS AND MACRON] +"\u022B" => "o" + +# ȭ [LATIN SMALL LETTER O WITH TILDE AND MACRON] +"\u022D" => "o" + +# ȯ [LATIN SMALL LETTER O WITH DOT ABOVE] +"\u022F" => "o" + +# ȱ [LATIN SMALL LETTER O WITH DOT ABOVE AND MACRON] +"\u0231" => "o" + +# ɔ [LATIN SMALL LETTER OPEN O] +"\u0254" => "o" + +# ɵ [LATIN SMALL LETTER BARRED O] +"\u0275" => "o" + +# ᴖ [LATIN SMALL LETTER TOP HALF O] +"\u1D16" => "o" + +# ᴗ [LATIN SMALL LETTER BOTTOM HALF O] +"\u1D17" => "o" + +# ᶗ [LATIN SMALL LETTER OPEN O WITH RETROFLEX HOOK] +"\u1D97" => "o" + +# ṍ [LATIN SMALL LETTER O WITH TILDE AND ACUTE] +"\u1E4D" => "o" + +# ṏ [LATIN SMALL LETTER O WITH TILDE AND DIAERESIS] +"\u1E4F" => "o" + +# ṑ [LATIN SMALL LETTER O WITH MACRON AND GRAVE] +"\u1E51" => "o" + +# ṓ [LATIN SMALL LETTER O WITH MACRON AND ACUTE] +"\u1E53" => "o" + +# ọ [LATIN SMALL LETTER O WITH DOT BELOW] +"\u1ECD" => "o" + +# ỏ [LATIN SMALL LETTER O WITH HOOK ABOVE] +"\u1ECF" => "o" + +# ố [LATIN SMALL LETTER O WITH CIRCUMFLEX AND ACUTE] +"\u1ED1" => "o" + +# ồ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND GRAVE] +"\u1ED3" => "o" + +# ổ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE] +"\u1ED5" => "o" + +# ỗ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND TILDE] +"\u1ED7" => "o" + +# ộ [LATIN SMALL LETTER O WITH CIRCUMFLEX AND DOT BELOW] +"\u1ED9" => "o" + +# ớ [LATIN SMALL LETTER O WITH HORN AND ACUTE] +"\u1EDB" => "o" + +# ờ [LATIN SMALL LETTER O WITH HORN AND GRAVE] +"\u1EDD" => "o" + +# ở [LATIN SMALL LETTER O WITH HORN AND HOOK ABOVE] +"\u1EDF" => "o" + +# ỡ [LATIN SMALL LETTER O WITH HORN AND TILDE] +"\u1EE1" => "o" + +# ợ [LATIN SMALL LETTER O WITH HORN AND DOT BELOW] +"\u1EE3" => "o" + +# ₒ [LATIN SUBSCRIPT SMALL LETTER O] +"\u2092" => "o" + +# ⓞ [CIRCLED LATIN SMALL LETTER O] +"\u24DE" => "o" + +# ⱺ [LATIN SMALL LETTER O WITH LOW RING INSIDE] +"\u2C7A" => "o" + +# ꝋ [LATIN SMALL LETTER O WITH LONG STROKE OVERLAY] +"\uA74B" => "o" + +# ꝍ [LATIN SMALL LETTER O WITH LOOP] +"\uA74D" => "o" + +# o [FULLWIDTH LATIN SMALL LETTER O] +"\uFF4F" => "o" + +# Œ [LATIN CAPITAL LIGATURE OE] +"\u0152" => "OE" + +# ɶ [LATIN LETTER SMALL CAPITAL OE] +"\u0276" => "OE" + +# Ꝏ [LATIN CAPITAL LETTER OO] +"\uA74E" => "OO" + +# Ȣ http://en.wikipedia.org/wiki/OU [LATIN CAPITAL LETTER OU] +"\u0222" => "OU" + +# ᴕ [LATIN LETTER SMALL CAPITAL OU] +"\u1D15" => "OU" + +# ⒪ [PARENTHESIZED LATIN SMALL LETTER O] +"\u24AA" => "(o)" + +# œ [LATIN SMALL LIGATURE OE] +"\u0153" => "oe" + +# ᴔ [LATIN SMALL LETTER TURNED OE] +"\u1D14" => "oe" + +# ꝏ [LATIN SMALL LETTER OO] +"\uA74F" => "oo" + +# ȣ http://en.wikipedia.org/wiki/OU [LATIN SMALL LETTER OU] +"\u0223" => "ou" + +# Ƥ [LATIN CAPITAL LETTER P WITH HOOK] +"\u01A4" => "P" + +# ᴘ [LATIN LETTER SMALL CAPITAL P] +"\u1D18" => "P" + +# Ṕ [LATIN CAPITAL LETTER P WITH ACUTE] +"\u1E54" => "P" + +# Ṗ [LATIN CAPITAL LETTER P WITH DOT ABOVE] +"\u1E56" => "P" + +# Ⓟ [CIRCLED LATIN CAPITAL LETTER P] +"\u24C5" => "P" + +# Ᵽ [LATIN CAPITAL LETTER P WITH STROKE] +"\u2C63" => "P" + +# Ꝑ [LATIN CAPITAL LETTER P WITH STROKE THROUGH DESCENDER] +"\uA750" => "P" + +# Ꝓ [LATIN CAPITAL LETTER P WITH FLOURISH] +"\uA752" => "P" + +# Ꝕ [LATIN CAPITAL LETTER P WITH SQUIRREL TAIL] +"\uA754" => "P" + +# P [FULLWIDTH LATIN CAPITAL LETTER P] +"\uFF30" => "P" + +# ƥ [LATIN SMALL LETTER P WITH HOOK] +"\u01A5" => "p" + +# ᵱ [LATIN SMALL LETTER P WITH MIDDLE TILDE] +"\u1D71" => "p" + +# ᵽ [LATIN SMALL LETTER P WITH STROKE] +"\u1D7D" => "p" + +# ᶈ [LATIN SMALL LETTER P WITH PALATAL HOOK] +"\u1D88" => "p" + +# ṕ [LATIN SMALL LETTER P WITH ACUTE] +"\u1E55" => "p" + +# ṗ [LATIN SMALL LETTER P WITH DOT ABOVE] +"\u1E57" => "p" + +# ⓟ [CIRCLED LATIN SMALL LETTER P] +"\u24DF" => "p" + +# ꝑ [LATIN SMALL LETTER P WITH STROKE THROUGH DESCENDER] +"\uA751" => "p" + +# ꝓ [LATIN SMALL LETTER P WITH FLOURISH] +"\uA753" => "p" + +# ꝕ [LATIN SMALL LETTER P WITH SQUIRREL TAIL] +"\uA755" => "p" + +# ꟼ [LATIN EPIGRAPHIC LETTER REVERSED P] +"\uA7FC" => "p" + +# p [FULLWIDTH LATIN SMALL LETTER P] +"\uFF50" => "p" + +# ⒫ [PARENTHESIZED LATIN SMALL LETTER P] +"\u24AB" => "(p)" + +# Ɋ [LATIN CAPITAL LETTER SMALL Q WITH HOOK TAIL] +"\u024A" => "Q" + +# Ⓠ [CIRCLED LATIN CAPITAL LETTER Q] +"\u24C6" => "Q" + +# Ꝗ [LATIN CAPITAL LETTER Q WITH STROKE THROUGH DESCENDER] +"\uA756" => "Q" + +# Ꝙ [LATIN CAPITAL LETTER Q WITH DIAGONAL STROKE] +"\uA758" => "Q" + +# Q [FULLWIDTH LATIN CAPITAL LETTER Q] +"\uFF31" => "Q" + +# ĸ http://en.wikipedia.org/wiki/Kra_(letter) [LATIN SMALL LETTER KRA] +"\u0138" => "q" + +# ɋ [LATIN SMALL LETTER Q WITH HOOK TAIL] +"\u024B" => "q" + +# ʠ [LATIN SMALL LETTER Q WITH HOOK] +"\u02A0" => "q" + +# ⓠ [CIRCLED LATIN SMALL LETTER Q] +"\u24E0" => "q" + +# ꝗ [LATIN SMALL LETTER Q WITH STROKE THROUGH DESCENDER] +"\uA757" => "q" + +# ꝙ [LATIN SMALL LETTER Q WITH DIAGONAL STROKE] +"\uA759" => "q" + +# q [FULLWIDTH LATIN SMALL LETTER Q] +"\uFF51" => "q" + +# ⒬ [PARENTHESIZED LATIN SMALL LETTER Q] +"\u24AC" => "(q)" + +# ȹ [LATIN SMALL LETTER QP DIGRAPH] +"\u0239" => "qp" + +# Ŕ [LATIN CAPITAL LETTER R WITH ACUTE] +"\u0154" => "R" + +# Ŗ [LATIN CAPITAL LETTER R WITH CEDILLA] +"\u0156" => "R" + +# Ř [LATIN CAPITAL LETTER R WITH CARON] +"\u0158" => "R" + +# Ȓ [LATIN CAPITAL LETTER R WITH DOUBLE GRAVE] +"\u0210" => "R" + +# Ȓ [LATIN CAPITAL LETTER R WITH INVERTED BREVE] +"\u0212" => "R" + +# Ɍ [LATIN CAPITAL LETTER R WITH STROKE] +"\u024C" => "R" + +# ʀ [LATIN LETTER SMALL CAPITAL R] +"\u0280" => "R" + +# ʁ [LATIN LETTER SMALL CAPITAL INVERTED R] +"\u0281" => "R" + +# ᴙ [LATIN LETTER SMALL CAPITAL REVERSED R] +"\u1D19" => "R" + +# ᴚ [LATIN LETTER SMALL CAPITAL TURNED R] +"\u1D1A" => "R" + +# Ṙ [LATIN CAPITAL LETTER R WITH DOT ABOVE] +"\u1E58" => "R" + +# Ṛ [LATIN CAPITAL LETTER R WITH DOT BELOW] +"\u1E5A" => "R" + +# Ṝ [LATIN CAPITAL LETTER R WITH DOT BELOW AND MACRON] +"\u1E5C" => "R" + +# Ṟ [LATIN CAPITAL LETTER R WITH LINE BELOW] +"\u1E5E" => "R" + +# Ⓡ [CIRCLED LATIN CAPITAL LETTER R] +"\u24C7" => "R" + +# Ɽ [LATIN CAPITAL LETTER R WITH TAIL] +"\u2C64" => "R" + +# Ꝛ [LATIN CAPITAL LETTER R ROTUNDA] +"\uA75A" => "R" + +# Ꞃ [LATIN CAPITAL LETTER INSULAR R] +"\uA782" => "R" + +# R [FULLWIDTH LATIN CAPITAL LETTER R] +"\uFF32" => "R" + +# ŕ [LATIN SMALL LETTER R WITH ACUTE] +"\u0155" => "r" + +# ŗ [LATIN SMALL LETTER R WITH CEDILLA] +"\u0157" => "r" + +# ř [LATIN SMALL LETTER R WITH CARON] +"\u0159" => "r" + +# ȑ [LATIN SMALL LETTER R WITH DOUBLE GRAVE] +"\u0211" => "r" + +# ȓ [LATIN SMALL LETTER R WITH INVERTED BREVE] +"\u0213" => "r" + +# ɍ [LATIN SMALL LETTER R WITH STROKE] +"\u024D" => "r" + +# ɼ [LATIN SMALL LETTER R WITH LONG LEG] +"\u027C" => "r" + +# ɽ [LATIN SMALL LETTER R WITH TAIL] +"\u027D" => "r" + +# ɾ [LATIN SMALL LETTER R WITH FISHHOOK] +"\u027E" => "r" + +# ɿ [LATIN SMALL LETTER REVERSED R WITH FISHHOOK] +"\u027F" => "r" + +# ᵣ [LATIN SUBSCRIPT SMALL LETTER R] +"\u1D63" => "r" + +# ᵲ [LATIN SMALL LETTER R WITH MIDDLE TILDE] +"\u1D72" => "r" + +# ᵳ [LATIN SMALL LETTER R WITH FISHHOOK AND MIDDLE TILDE] +"\u1D73" => "r" + +# ᶉ [LATIN SMALL LETTER R WITH PALATAL HOOK] +"\u1D89" => "r" + +# ṙ [LATIN SMALL LETTER R WITH DOT ABOVE] +"\u1E59" => "r" + +# ṛ [LATIN SMALL LETTER R WITH DOT BELOW] +"\u1E5B" => "r" + +# ṝ [LATIN SMALL LETTER R WITH DOT BELOW AND MACRON] +"\u1E5D" => "r" + +# ṟ [LATIN SMALL LETTER R WITH LINE BELOW] +"\u1E5F" => "r" + +# ⓡ [CIRCLED LATIN SMALL LETTER R] +"\u24E1" => "r" + +# ꝛ [LATIN SMALL LETTER R ROTUNDA] +"\uA75B" => "r" + +# ꞃ [LATIN SMALL LETTER INSULAR R] +"\uA783" => "r" + +# r [FULLWIDTH LATIN SMALL LETTER R] +"\uFF52" => "r" + +# ⒭ [PARENTHESIZED LATIN SMALL LETTER R] +"\u24AD" => "(r)" + +# Ś [LATIN CAPITAL LETTER S WITH ACUTE] +"\u015A" => "S" + +# Ŝ [LATIN CAPITAL LETTER S WITH CIRCUMFLEX] +"\u015C" => "S" + +# Ş [LATIN CAPITAL LETTER S WITH CEDILLA] +"\u015E" => "S" + +# Š [LATIN CAPITAL LETTER S WITH CARON] +"\u0160" => "S" + +# Ș [LATIN CAPITAL LETTER S WITH COMMA BELOW] +"\u0218" => "S" + +# Ṡ [LATIN CAPITAL LETTER S WITH DOT ABOVE] +"\u1E60" => "S" + +# Ṣ [LATIN CAPITAL LETTER S WITH DOT BELOW] +"\u1E62" => "S" + +# Ṥ [LATIN CAPITAL LETTER S WITH ACUTE AND DOT ABOVE] +"\u1E64" => "S" + +# Ṧ [LATIN CAPITAL LETTER S WITH CARON AND DOT ABOVE] +"\u1E66" => "S" + +# Ṩ [LATIN CAPITAL LETTER S WITH DOT BELOW AND DOT ABOVE] +"\u1E68" => "S" + +# Ⓢ [CIRCLED LATIN CAPITAL LETTER S] +"\u24C8" => "S" + +# ꜱ [LATIN LETTER SMALL CAPITAL S] +"\uA731" => "S" + +# ꞅ [LATIN SMALL LETTER INSULAR S] +"\uA785" => "S" + +# S [FULLWIDTH LATIN CAPITAL LETTER S] +"\uFF33" => "S" + +# ś [LATIN SMALL LETTER S WITH ACUTE] +"\u015B" => "s" + +# ŝ [LATIN SMALL LETTER S WITH CIRCUMFLEX] +"\u015D" => "s" + +# ş [LATIN SMALL LETTER S WITH CEDILLA] +"\u015F" => "s" + +# š [LATIN SMALL LETTER S WITH CARON] +"\u0161" => "s" + +# ſ http://en.wikipedia.org/wiki/Long_S [LATIN SMALL LETTER LONG S] +"\u017F" => "s" + +# ș [LATIN SMALL LETTER S WITH COMMA BELOW] +"\u0219" => "s" + +# ȿ [LATIN SMALL LETTER S WITH SWASH TAIL] +"\u023F" => "s" + +# ʂ [LATIN SMALL LETTER S WITH HOOK] +"\u0282" => "s" + +# ᵴ [LATIN SMALL LETTER S WITH MIDDLE TILDE] +"\u1D74" => "s" + +# ᶊ [LATIN SMALL LETTER S WITH PALATAL HOOK] +"\u1D8A" => "s" + +# ṡ [LATIN SMALL LETTER S WITH DOT ABOVE] +"\u1E61" => "s" + +# ṣ [LATIN SMALL LETTER S WITH DOT BELOW] +"\u1E63" => "s" + +# ṥ [LATIN SMALL LETTER S WITH ACUTE AND DOT ABOVE] +"\u1E65" => "s" + +# ṧ [LATIN SMALL LETTER S WITH CARON AND DOT ABOVE] +"\u1E67" => "s" + +# ṩ [LATIN SMALL LETTER S WITH DOT BELOW AND DOT ABOVE] +"\u1E69" => "s" + +# ẜ [LATIN SMALL LETTER LONG S WITH DIAGONAL STROKE] +"\u1E9C" => "s" + +# ẝ [LATIN SMALL LETTER LONG S WITH HIGH STROKE] +"\u1E9D" => "s" + +# ⓢ [CIRCLED LATIN SMALL LETTER S] +"\u24E2" => "s" + +# Ꞅ [LATIN CAPITAL LETTER INSULAR S] +"\uA784" => "s" + +# s [FULLWIDTH LATIN SMALL LETTER S] +"\uFF53" => "s" + +# ẞ [LATIN CAPITAL LETTER SHARP S] +"\u1E9E" => "SS" + +# ⒮ [PARENTHESIZED LATIN SMALL LETTER S] +"\u24AE" => "(s)" + +# ß [LATIN SMALL LETTER SHARP S] +"\u00DF" => "ss" + +# st [LATIN SMALL LIGATURE ST] +"\uFB06" => "st" + +# Ţ [LATIN CAPITAL LETTER T WITH CEDILLA] +"\u0162" => "T" + +# Ť [LATIN CAPITAL LETTER T WITH CARON] +"\u0164" => "T" + +# Ŧ [LATIN CAPITAL LETTER T WITH STROKE] +"\u0166" => "T" + +# Ƭ [LATIN CAPITAL LETTER T WITH HOOK] +"\u01AC" => "T" + +# Ʈ [LATIN CAPITAL LETTER T WITH RETROFLEX HOOK] +"\u01AE" => "T" + +# Ț [LATIN CAPITAL LETTER T WITH COMMA BELOW] +"\u021A" => "T" + +# Ⱦ [LATIN CAPITAL LETTER T WITH DIAGONAL STROKE] +"\u023E" => "T" + +# ᴛ [LATIN LETTER SMALL CAPITAL T] +"\u1D1B" => "T" + +# Ṫ [LATIN CAPITAL LETTER T WITH DOT ABOVE] +"\u1E6A" => "T" + +# Ṭ [LATIN CAPITAL LETTER T WITH DOT BELOW] +"\u1E6C" => "T" + +# Ṯ [LATIN CAPITAL LETTER T WITH LINE BELOW] +"\u1E6E" => "T" + +# Ṱ [LATIN CAPITAL LETTER T WITH CIRCUMFLEX BELOW] +"\u1E70" => "T" + +# Ⓣ [CIRCLED LATIN CAPITAL LETTER T] +"\u24C9" => "T" + +# Ꞇ [LATIN CAPITAL LETTER INSULAR T] +"\uA786" => "T" + +# T [FULLWIDTH LATIN CAPITAL LETTER T] +"\uFF34" => "T" + +# ţ [LATIN SMALL LETTER T WITH CEDILLA] +"\u0163" => "t" + +# ť [LATIN SMALL LETTER T WITH CARON] +"\u0165" => "t" + +# ŧ [LATIN SMALL LETTER T WITH STROKE] +"\u0167" => "t" + +# ƫ [LATIN SMALL LETTER T WITH PALATAL HOOK] +"\u01AB" => "t" + +# ƭ [LATIN SMALL LETTER T WITH HOOK] +"\u01AD" => "t" + +# ț [LATIN SMALL LETTER T WITH COMMA BELOW] +"\u021B" => "t" + +# ȶ [LATIN SMALL LETTER T WITH CURL] +"\u0236" => "t" + +# ʇ [LATIN SMALL LETTER TURNED T] +"\u0287" => "t" + +# ʈ [LATIN SMALL LETTER T WITH RETROFLEX HOOK] +"\u0288" => "t" + +# ᵵ [LATIN SMALL LETTER T WITH MIDDLE TILDE] +"\u1D75" => "t" + +# ṫ [LATIN SMALL LETTER T WITH DOT ABOVE] +"\u1E6B" => "t" + +# ṭ [LATIN SMALL LETTER T WITH DOT BELOW] +"\u1E6D" => "t" + +# ṯ [LATIN SMALL LETTER T WITH LINE BELOW] +"\u1E6F" => "t" + +# ṱ [LATIN SMALL LETTER T WITH CIRCUMFLEX BELOW] +"\u1E71" => "t" + +# ẗ [LATIN SMALL LETTER T WITH DIAERESIS] +"\u1E97" => "t" + +# ⓣ [CIRCLED LATIN SMALL LETTER T] +"\u24E3" => "t" + +# ⱦ [LATIN SMALL LETTER T WITH DIAGONAL STROKE] +"\u2C66" => "t" + +# t [FULLWIDTH LATIN SMALL LETTER T] +"\uFF54" => "t" + +# Þ [LATIN CAPITAL LETTER THORN] +"\u00DE" => "TH" + +# Ꝧ [LATIN CAPITAL LETTER THORN WITH STROKE THROUGH DESCENDER] +"\uA766" => "TH" + +# Ꜩ [LATIN CAPITAL LETTER TZ] +"\uA728" => "TZ" + +# ⒯ [PARENTHESIZED LATIN SMALL LETTER T] +"\u24AF" => "(t)" + +# ʨ [LATIN SMALL LETTER TC DIGRAPH WITH CURL] +"\u02A8" => "tc" + +# þ [LATIN SMALL LETTER THORN] +"\u00FE" => "th" + +# ᵺ [LATIN SMALL LETTER TH WITH STRIKETHROUGH] +"\u1D7A" => "th" + +# ꝧ [LATIN SMALL LETTER THORN WITH STROKE THROUGH DESCENDER] +"\uA767" => "th" + +# ʦ [LATIN SMALL LETTER TS DIGRAPH] +"\u02A6" => "ts" + +# ꜩ [LATIN SMALL LETTER TZ] +"\uA729" => "tz" + +# Ù [LATIN CAPITAL LETTER U WITH GRAVE] +"\u00D9" => "U" + +# Ú [LATIN CAPITAL LETTER U WITH ACUTE] +"\u00DA" => "U" + +# Û [LATIN CAPITAL LETTER U WITH CIRCUMFLEX] +"\u00DB" => "U" + +# Ü [LATIN CAPITAL LETTER U WITH DIAERESIS] +"\u00DC" => "U" + +# Ũ [LATIN CAPITAL LETTER U WITH TILDE] +"\u0168" => "U" + +# Ū [LATIN CAPITAL LETTER U WITH MACRON] +"\u016A" => "U" + +# Ŭ [LATIN CAPITAL LETTER U WITH BREVE] +"\u016C" => "U" + +# Ů [LATIN CAPITAL LETTER U WITH RING ABOVE] +"\u016E" => "U" + +# Ű [LATIN CAPITAL LETTER U WITH DOUBLE ACUTE] +"\u0170" => "U" + +# Ų [LATIN CAPITAL LETTER U WITH OGONEK] +"\u0172" => "U" + +# Ư [LATIN CAPITAL LETTER U WITH HORN] +"\u01AF" => "U" + +# Ǔ [LATIN CAPITAL LETTER U WITH CARON] +"\u01D3" => "U" + +# Ǖ [LATIN CAPITAL LETTER U WITH DIAERESIS AND MACRON] +"\u01D5" => "U" + +# Ǘ [LATIN CAPITAL LETTER U WITH DIAERESIS AND ACUTE] +"\u01D7" => "U" + +# Ǚ [LATIN CAPITAL LETTER U WITH DIAERESIS AND CARON] +"\u01D9" => "U" + +# Ǜ [LATIN CAPITAL LETTER U WITH DIAERESIS AND GRAVE] +"\u01DB" => "U" + +# Ȕ [LATIN CAPITAL LETTER U WITH DOUBLE GRAVE] +"\u0214" => "U" + +# Ȗ [LATIN CAPITAL LETTER U WITH INVERTED BREVE] +"\u0216" => "U" + +# Ʉ [LATIN CAPITAL LETTER U BAR] +"\u0244" => "U" + +# ᴜ [LATIN LETTER SMALL CAPITAL U] +"\u1D1C" => "U" + +# ᵾ [LATIN SMALL CAPITAL LETTER U WITH STROKE] +"\u1D7E" => "U" + +# Ṳ [LATIN CAPITAL LETTER U WITH DIAERESIS BELOW] +"\u1E72" => "U" + +# Ṵ [LATIN CAPITAL LETTER U WITH TILDE BELOW] +"\u1E74" => "U" + +# Ṷ [LATIN CAPITAL LETTER U WITH CIRCUMFLEX BELOW] +"\u1E76" => "U" + +# Ṹ [LATIN CAPITAL LETTER U WITH TILDE AND ACUTE] +"\u1E78" => "U" + +# Ṻ [LATIN CAPITAL LETTER U WITH MACRON AND DIAERESIS] +"\u1E7A" => "U" + +# Ụ [LATIN CAPITAL LETTER U WITH DOT BELOW] +"\u1EE4" => "U" + +# Ủ [LATIN CAPITAL LETTER U WITH HOOK ABOVE] +"\u1EE6" => "U" + +# Ứ [LATIN CAPITAL LETTER U WITH HORN AND ACUTE] +"\u1EE8" => "U" + +# Ừ [LATIN CAPITAL LETTER U WITH HORN AND GRAVE] +"\u1EEA" => "U" + +# Ử [LATIN CAPITAL LETTER U WITH HORN AND HOOK ABOVE] +"\u1EEC" => "U" + +# Ữ [LATIN CAPITAL LETTER U WITH HORN AND TILDE] +"\u1EEE" => "U" + +# Ự [LATIN CAPITAL LETTER U WITH HORN AND DOT BELOW] +"\u1EF0" => "U" + +# Ⓤ [CIRCLED LATIN CAPITAL LETTER U] +"\u24CA" => "U" + +# U [FULLWIDTH LATIN CAPITAL LETTER U] +"\uFF35" => "U" + +# ù [LATIN SMALL LETTER U WITH GRAVE] +"\u00F9" => "u" + +# ú [LATIN SMALL LETTER U WITH ACUTE] +"\u00FA" => "u" + +# û [LATIN SMALL LETTER U WITH CIRCUMFLEX] +"\u00FB" => "u" + +# ü [LATIN SMALL LETTER U WITH DIAERESIS] +"\u00FC" => "u" + +# ũ [LATIN SMALL LETTER U WITH TILDE] +"\u0169" => "u" + +# ū [LATIN SMALL LETTER U WITH MACRON] +"\u016B" => "u" + +# ŭ [LATIN SMALL LETTER U WITH BREVE] +"\u016D" => "u" + +# ů [LATIN SMALL LETTER U WITH RING ABOVE] +"\u016F" => "u" + +# ű [LATIN SMALL LETTER U WITH DOUBLE ACUTE] +"\u0171" => "u" + +# ų [LATIN SMALL LETTER U WITH OGONEK] +"\u0173" => "u" + +# ư [LATIN SMALL LETTER U WITH HORN] +"\u01B0" => "u" + +# ǔ [LATIN SMALL LETTER U WITH CARON] +"\u01D4" => "u" + +# ǖ [LATIN SMALL LETTER U WITH DIAERESIS AND MACRON] +"\u01D6" => "u" + +# ǘ [LATIN SMALL LETTER U WITH DIAERESIS AND ACUTE] +"\u01D8" => "u" + +# ǚ [LATIN SMALL LETTER U WITH DIAERESIS AND CARON] +"\u01DA" => "u" + +# ǜ [LATIN SMALL LETTER U WITH DIAERESIS AND GRAVE] +"\u01DC" => "u" + +# ȕ [LATIN SMALL LETTER U WITH DOUBLE GRAVE] +"\u0215" => "u" + +# ȗ [LATIN SMALL LETTER U WITH INVERTED BREVE] +"\u0217" => "u" + +# ʉ [LATIN SMALL LETTER U BAR] +"\u0289" => "u" + +# ᵤ [LATIN SUBSCRIPT SMALL LETTER U] +"\u1D64" => "u" + +# ᶙ [LATIN SMALL LETTER U WITH RETROFLEX HOOK] +"\u1D99" => "u" + +# ṳ [LATIN SMALL LETTER U WITH DIAERESIS BELOW] +"\u1E73" => "u" + +# ṵ [LATIN SMALL LETTER U WITH TILDE BELOW] +"\u1E75" => "u" + +# ṷ [LATIN SMALL LETTER U WITH CIRCUMFLEX BELOW] +"\u1E77" => "u" + +# ṹ [LATIN SMALL LETTER U WITH TILDE AND ACUTE] +"\u1E79" => "u" + +# ṻ [LATIN SMALL LETTER U WITH MACRON AND DIAERESIS] +"\u1E7B" => "u" + +# ụ [LATIN SMALL LETTER U WITH DOT BELOW] +"\u1EE5" => "u" + +# ủ [LATIN SMALL LETTER U WITH HOOK ABOVE] +"\u1EE7" => "u" + +# ứ [LATIN SMALL LETTER U WITH HORN AND ACUTE] +"\u1EE9" => "u" + +# ừ [LATIN SMALL LETTER U WITH HORN AND GRAVE] +"\u1EEB" => "u" + +# ử [LATIN SMALL LETTER U WITH HORN AND HOOK ABOVE] +"\u1EED" => "u" + +# ữ [LATIN SMALL LETTER U WITH HORN AND TILDE] +"\u1EEF" => "u" + +# ự [LATIN SMALL LETTER U WITH HORN AND DOT BELOW] +"\u1EF1" => "u" + +# ⓤ [CIRCLED LATIN SMALL LETTER U] +"\u24E4" => "u" + +# u [FULLWIDTH LATIN SMALL LETTER U] +"\uFF55" => "u" + +# ⒰ [PARENTHESIZED LATIN SMALL LETTER U] +"\u24B0" => "(u)" + +# ᵫ [LATIN SMALL LETTER UE] +"\u1D6B" => "ue" + +# Ʋ [LATIN CAPITAL LETTER V WITH HOOK] +"\u01B2" => "V" + +# Ʌ [LATIN CAPITAL LETTER TURNED V] +"\u0245" => "V" + +# ᴠ [LATIN LETTER SMALL CAPITAL V] +"\u1D20" => "V" + +# Ṽ [LATIN CAPITAL LETTER V WITH TILDE] +"\u1E7C" => "V" + +# Ṿ [LATIN CAPITAL LETTER V WITH DOT BELOW] +"\u1E7E" => "V" + +# Ỽ [LATIN CAPITAL LETTER MIDDLE-WELSH V] +"\u1EFC" => "V" + +# Ⓥ [CIRCLED LATIN CAPITAL LETTER V] +"\u24CB" => "V" + +# Ꝟ [LATIN CAPITAL LETTER V WITH DIAGONAL STROKE] +"\uA75E" => "V" + +# Ꝩ [LATIN CAPITAL LETTER VEND] +"\uA768" => "V" + +# V [FULLWIDTH LATIN CAPITAL LETTER V] +"\uFF36" => "V" + +# ʋ [LATIN SMALL LETTER V WITH HOOK] +"\u028B" => "v" + +# ʌ [LATIN SMALL LETTER TURNED V] +"\u028C" => "v" + +# ᵥ [LATIN SUBSCRIPT SMALL LETTER V] +"\u1D65" => "v" + +# ᶌ [LATIN SMALL LETTER V WITH PALATAL HOOK] +"\u1D8C" => "v" + +# ṽ [LATIN SMALL LETTER V WITH TILDE] +"\u1E7D" => "v" + +# ṿ [LATIN SMALL LETTER V WITH DOT BELOW] +"\u1E7F" => "v" + +# ⓥ [CIRCLED LATIN SMALL LETTER V] +"\u24E5" => "v" + +# ⱱ [LATIN SMALL LETTER V WITH RIGHT HOOK] +"\u2C71" => "v" + +# ⱴ [LATIN SMALL LETTER V WITH CURL] +"\u2C74" => "v" + +# ꝟ [LATIN SMALL LETTER V WITH DIAGONAL STROKE] +"\uA75F" => "v" + +# v [FULLWIDTH LATIN SMALL LETTER V] +"\uFF56" => "v" + +# Ꝡ [LATIN CAPITAL LETTER VY] +"\uA760" => "VY" + +# ⒱ [PARENTHESIZED LATIN SMALL LETTER V] +"\u24B1" => "(v)" + +# ꝡ [LATIN SMALL LETTER VY] +"\uA761" => "vy" + +# Ŵ [LATIN CAPITAL LETTER W WITH CIRCUMFLEX] +"\u0174" => "W" + +# Ƿ http://en.wikipedia.org/wiki/Wynn [LATIN CAPITAL LETTER WYNN] +"\u01F7" => "W" + +# ᴡ [LATIN LETTER SMALL CAPITAL W] +"\u1D21" => "W" + +# Ẁ [LATIN CAPITAL LETTER W WITH GRAVE] +"\u1E80" => "W" + +# Ẃ [LATIN CAPITAL LETTER W WITH ACUTE] +"\u1E82" => "W" + +# Ẅ [LATIN CAPITAL LETTER W WITH DIAERESIS] +"\u1E84" => "W" + +# Ẇ [LATIN CAPITAL LETTER W WITH DOT ABOVE] +"\u1E86" => "W" + +# Ẉ [LATIN CAPITAL LETTER W WITH DOT BELOW] +"\u1E88" => "W" + +# Ⓦ [CIRCLED LATIN CAPITAL LETTER W] +"\u24CC" => "W" + +# Ⱳ [LATIN CAPITAL LETTER W WITH HOOK] +"\u2C72" => "W" + +# W [FULLWIDTH LATIN CAPITAL LETTER W] +"\uFF37" => "W" + +# ŵ [LATIN SMALL LETTER W WITH CIRCUMFLEX] +"\u0175" => "w" + +# ƿ http://en.wikipedia.org/wiki/Wynn [LATIN LETTER WYNN] +"\u01BF" => "w" + +# ʍ [LATIN SMALL LETTER TURNED W] +"\u028D" => "w" + +# ẁ [LATIN SMALL LETTER W WITH GRAVE] +"\u1E81" => "w" + +# ẃ [LATIN SMALL LETTER W WITH ACUTE] +"\u1E83" => "w" + +# ẅ [LATIN SMALL LETTER W WITH DIAERESIS] +"\u1E85" => "w" + +# ẇ [LATIN SMALL LETTER W WITH DOT ABOVE] +"\u1E87" => "w" + +# ẉ [LATIN SMALL LETTER W WITH DOT BELOW] +"\u1E89" => "w" + +# ẘ [LATIN SMALL LETTER W WITH RING ABOVE] +"\u1E98" => "w" + +# ⓦ [CIRCLED LATIN SMALL LETTER W] +"\u24E6" => "w" + +# ⱳ [LATIN SMALL LETTER W WITH HOOK] +"\u2C73" => "w" + +# w [FULLWIDTH LATIN SMALL LETTER W] +"\uFF57" => "w" + +# ⒲ [PARENTHESIZED LATIN SMALL LETTER W] +"\u24B2" => "(w)" + +# Ẋ [LATIN CAPITAL LETTER X WITH DOT ABOVE] +"\u1E8A" => "X" + +# Ẍ [LATIN CAPITAL LETTER X WITH DIAERESIS] +"\u1E8C" => "X" + +# Ⓧ [CIRCLED LATIN CAPITAL LETTER X] +"\u24CD" => "X" + +# X [FULLWIDTH LATIN CAPITAL LETTER X] +"\uFF38" => "X" + +# ᶍ [LATIN SMALL LETTER X WITH PALATAL HOOK] +"\u1D8D" => "x" + +# ẋ [LATIN SMALL LETTER X WITH DOT ABOVE] +"\u1E8B" => "x" + +# ẍ [LATIN SMALL LETTER X WITH DIAERESIS] +"\u1E8D" => "x" + +# ₓ [LATIN SUBSCRIPT SMALL LETTER X] +"\u2093" => "x" + +# ⓧ [CIRCLED LATIN SMALL LETTER X] +"\u24E7" => "x" + +# x [FULLWIDTH LATIN SMALL LETTER X] +"\uFF58" => "x" + +# ⒳ [PARENTHESIZED LATIN SMALL LETTER X] +"\u24B3" => "(x)" + +# Ý [LATIN CAPITAL LETTER Y WITH ACUTE] +"\u00DD" => "Y" + +# Ŷ [LATIN CAPITAL LETTER Y WITH CIRCUMFLEX] +"\u0176" => "Y" + +# Ÿ [LATIN CAPITAL LETTER Y WITH DIAERESIS] +"\u0178" => "Y" + +# Ƴ [LATIN CAPITAL LETTER Y WITH HOOK] +"\u01B3" => "Y" + +# Ȳ [LATIN CAPITAL LETTER Y WITH MACRON] +"\u0232" => "Y" + +# Ɏ [LATIN CAPITAL LETTER Y WITH STROKE] +"\u024E" => "Y" + +# ʏ [LATIN LETTER SMALL CAPITAL Y] +"\u028F" => "Y" + +# Ẏ [LATIN CAPITAL LETTER Y WITH DOT ABOVE] +"\u1E8E" => "Y" + +# Ỳ [LATIN CAPITAL LETTER Y WITH GRAVE] +"\u1EF2" => "Y" + +# Ỵ [LATIN CAPITAL LETTER Y WITH DOT BELOW] +"\u1EF4" => "Y" + +# Ỷ [LATIN CAPITAL LETTER Y WITH HOOK ABOVE] +"\u1EF6" => "Y" + +# Ỹ [LATIN CAPITAL LETTER Y WITH TILDE] +"\u1EF8" => "Y" + +# Ỿ [LATIN CAPITAL LETTER Y WITH LOOP] +"\u1EFE" => "Y" + +# Ⓨ [CIRCLED LATIN CAPITAL LETTER Y] +"\u24CE" => "Y" + +# Y [FULLWIDTH LATIN CAPITAL LETTER Y] +"\uFF39" => "Y" + +# ý [LATIN SMALL LETTER Y WITH ACUTE] +"\u00FD" => "y" + +# ÿ [LATIN SMALL LETTER Y WITH DIAERESIS] +"\u00FF" => "y" + +# ŷ [LATIN SMALL LETTER Y WITH CIRCUMFLEX] +"\u0177" => "y" + +# ƴ [LATIN SMALL LETTER Y WITH HOOK] +"\u01B4" => "y" + +# ȳ [LATIN SMALL LETTER Y WITH MACRON] +"\u0233" => "y" + +# ɏ [LATIN SMALL LETTER Y WITH STROKE] +"\u024F" => "y" + +# ʎ [LATIN SMALL LETTER TURNED Y] +"\u028E" => "y" + +# ẏ [LATIN SMALL LETTER Y WITH DOT ABOVE] +"\u1E8F" => "y" + +# ẙ [LATIN SMALL LETTER Y WITH RING ABOVE] +"\u1E99" => "y" + +# ỳ [LATIN SMALL LETTER Y WITH GRAVE] +"\u1EF3" => "y" + +# ỵ [LATIN SMALL LETTER Y WITH DOT BELOW] +"\u1EF5" => "y" + +# ỷ [LATIN SMALL LETTER Y WITH HOOK ABOVE] +"\u1EF7" => "y" + +# ỹ [LATIN SMALL LETTER Y WITH TILDE] +"\u1EF9" => "y" + +# ỿ [LATIN SMALL LETTER Y WITH LOOP] +"\u1EFF" => "y" + +# ⓨ [CIRCLED LATIN SMALL LETTER Y] +"\u24E8" => "y" + +# y [FULLWIDTH LATIN SMALL LETTER Y] +"\uFF59" => "y" + +# ⒴ [PARENTHESIZED LATIN SMALL LETTER Y] +"\u24B4" => "(y)" + +# Ź [LATIN CAPITAL LETTER Z WITH ACUTE] +"\u0179" => "Z" + +# Ż [LATIN CAPITAL LETTER Z WITH DOT ABOVE] +"\u017B" => "Z" + +# Ž [LATIN CAPITAL LETTER Z WITH CARON] +"\u017D" => "Z" + +# Ƶ [LATIN CAPITAL LETTER Z WITH STROKE] +"\u01B5" => "Z" + +# Ȝ http://en.wikipedia.org/wiki/Yogh [LATIN CAPITAL LETTER YOGH] +"\u021C" => "Z" + +# Ȥ [LATIN CAPITAL LETTER Z WITH HOOK] +"\u0224" => "Z" + +# ᴢ [LATIN LETTER SMALL CAPITAL Z] +"\u1D22" => "Z" + +# Ẑ [LATIN CAPITAL LETTER Z WITH CIRCUMFLEX] +"\u1E90" => "Z" + +# Ẓ [LATIN CAPITAL LETTER Z WITH DOT BELOW] +"\u1E92" => "Z" + +# Ẕ [LATIN CAPITAL LETTER Z WITH LINE BELOW] +"\u1E94" => "Z" + +# Ⓩ [CIRCLED LATIN CAPITAL LETTER Z] +"\u24CF" => "Z" + +# Ⱬ [LATIN CAPITAL LETTER Z WITH DESCENDER] +"\u2C6B" => "Z" + +# Ꝣ [LATIN CAPITAL LETTER VISIGOTHIC Z] +"\uA762" => "Z" + +# Z [FULLWIDTH LATIN CAPITAL LETTER Z] +"\uFF3A" => "Z" + +# ź [LATIN SMALL LETTER Z WITH ACUTE] +"\u017A" => "z" + +# ż [LATIN SMALL LETTER Z WITH DOT ABOVE] +"\u017C" => "z" + +# ž [LATIN SMALL LETTER Z WITH CARON] +"\u017E" => "z" + +# ƶ [LATIN SMALL LETTER Z WITH STROKE] +"\u01B6" => "z" + +# ȝ http://en.wikipedia.org/wiki/Yogh [LATIN SMALL LETTER YOGH] +"\u021D" => "z" + +# ȥ [LATIN SMALL LETTER Z WITH HOOK] +"\u0225" => "z" + +# ɀ [LATIN SMALL LETTER Z WITH SWASH TAIL] +"\u0240" => "z" + +# ʐ [LATIN SMALL LETTER Z WITH RETROFLEX HOOK] +"\u0290" => "z" + +# ʑ [LATIN SMALL LETTER Z WITH CURL] +"\u0291" => "z" + +# ᵶ [LATIN SMALL LETTER Z WITH MIDDLE TILDE] +"\u1D76" => "z" + +# ᶎ [LATIN SMALL LETTER Z WITH PALATAL HOOK] +"\u1D8E" => "z" + +# ẑ [LATIN SMALL LETTER Z WITH CIRCUMFLEX] +"\u1E91" => "z" + +# ẓ [LATIN SMALL LETTER Z WITH DOT BELOW] +"\u1E93" => "z" + +# ẕ [LATIN SMALL LETTER Z WITH LINE BELOW] +"\u1E95" => "z" + +# ⓩ [CIRCLED LATIN SMALL LETTER Z] +"\u24E9" => "z" + +# ⱬ [LATIN SMALL LETTER Z WITH DESCENDER] +"\u2C6C" => "z" + +# ꝣ [LATIN SMALL LETTER VISIGOTHIC Z] +"\uA763" => "z" + +# z [FULLWIDTH LATIN SMALL LETTER Z] +"\uFF5A" => "z" + +# ⒵ [PARENTHESIZED LATIN SMALL LETTER Z] +"\u24B5" => "(z)" + +# ⁰ [SUPERSCRIPT ZERO] +"\u2070" => "0" + +# ₀ [SUBSCRIPT ZERO] +"\u2080" => "0" + +# ⓪ [CIRCLED DIGIT ZERO] +"\u24EA" => "0" + +# ⓿ [NEGATIVE CIRCLED DIGIT ZERO] +"\u24FF" => "0" + +# 0 [FULLWIDTH DIGIT ZERO] +"\uFF10" => "0" + +# ¹ [SUPERSCRIPT ONE] +"\u00B9" => "1" + +# ₁ [SUBSCRIPT ONE] +"\u2081" => "1" + +# ① [CIRCLED DIGIT ONE] +"\u2460" => "1" + +# ⓵ [DOUBLE CIRCLED DIGIT ONE] +"\u24F5" => "1" + +# ❶ [DINGBAT NEGATIVE CIRCLED DIGIT ONE] +"\u2776" => "1" + +# ➀ [DINGBAT CIRCLED SANS-SERIF DIGIT ONE] +"\u2780" => "1" + +# ➊ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT ONE] +"\u278A" => "1" + +# 1 [FULLWIDTH DIGIT ONE] +"\uFF11" => "1" + +# ⒈ [DIGIT ONE FULL STOP] +"\u2488" => "1." + +# ⑴ [PARENTHESIZED DIGIT ONE] +"\u2474" => "(1)" + +# ² [SUPERSCRIPT TWO] +"\u00B2" => "2" + +# ₂ [SUBSCRIPT TWO] +"\u2082" => "2" + +# ② [CIRCLED DIGIT TWO] +"\u2461" => "2" + +# ⓶ [DOUBLE CIRCLED DIGIT TWO] +"\u24F6" => "2" + +# ❷ [DINGBAT NEGATIVE CIRCLED DIGIT TWO] +"\u2777" => "2" + +# ➁ [DINGBAT CIRCLED SANS-SERIF DIGIT TWO] +"\u2781" => "2" + +# ➋ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT TWO] +"\u278B" => "2" + +# 2 [FULLWIDTH DIGIT TWO] +"\uFF12" => "2" + +# ⒉ [DIGIT TWO FULL STOP] +"\u2489" => "2." + +# ⑵ [PARENTHESIZED DIGIT TWO] +"\u2475" => "(2)" + +# ³ [SUPERSCRIPT THREE] +"\u00B3" => "3" + +# ₃ [SUBSCRIPT THREE] +"\u2083" => "3" + +# ③ [CIRCLED DIGIT THREE] +"\u2462" => "3" + +# ⓷ [DOUBLE CIRCLED DIGIT THREE] +"\u24F7" => "3" + +# ❸ [DINGBAT NEGATIVE CIRCLED DIGIT THREE] +"\u2778" => "3" + +# ➂ [DINGBAT CIRCLED SANS-SERIF DIGIT THREE] +"\u2782" => "3" + +# ➌ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT THREE] +"\u278C" => "3" + +# 3 [FULLWIDTH DIGIT THREE] +"\uFF13" => "3" + +# ⒊ [DIGIT THREE FULL STOP] +"\u248A" => "3." + +# ⑶ [PARENTHESIZED DIGIT THREE] +"\u2476" => "(3)" + +# ⁴ [SUPERSCRIPT FOUR] +"\u2074" => "4" + +# ₄ [SUBSCRIPT FOUR] +"\u2084" => "4" + +# ④ [CIRCLED DIGIT FOUR] +"\u2463" => "4" + +# ⓸ [DOUBLE CIRCLED DIGIT FOUR] +"\u24F8" => "4" + +# ❹ [DINGBAT NEGATIVE CIRCLED DIGIT FOUR] +"\u2779" => "4" + +# ➃ [DINGBAT CIRCLED SANS-SERIF DIGIT FOUR] +"\u2783" => "4" + +# ➍ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT FOUR] +"\u278D" => "4" + +# 4 [FULLWIDTH DIGIT FOUR] +"\uFF14" => "4" + +# ⒋ [DIGIT FOUR FULL STOP] +"\u248B" => "4." + +# ⑷ [PARENTHESIZED DIGIT FOUR] +"\u2477" => "(4)" + +# ⁵ [SUPERSCRIPT FIVE] +"\u2075" => "5" + +# ₅ [SUBSCRIPT FIVE] +"\u2085" => "5" + +# ⑤ [CIRCLED DIGIT FIVE] +"\u2464" => "5" + +# ⓹ [DOUBLE CIRCLED DIGIT FIVE] +"\u24F9" => "5" + +# ❺ [DINGBAT NEGATIVE CIRCLED DIGIT FIVE] +"\u277A" => "5" + +# ➄ [DINGBAT CIRCLED SANS-SERIF DIGIT FIVE] +"\u2784" => "5" + +# ➎ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT FIVE] +"\u278E" => "5" + +# 5 [FULLWIDTH DIGIT FIVE] +"\uFF15" => "5" + +# ⒌ [DIGIT FIVE FULL STOP] +"\u248C" => "5." + +# ⑸ [PARENTHESIZED DIGIT FIVE] +"\u2478" => "(5)" + +# ⁶ [SUPERSCRIPT SIX] +"\u2076" => "6" + +# ₆ [SUBSCRIPT SIX] +"\u2086" => "6" + +# ⑥ [CIRCLED DIGIT SIX] +"\u2465" => "6" + +# ⓺ [DOUBLE CIRCLED DIGIT SIX] +"\u24FA" => "6" + +# ❻ [DINGBAT NEGATIVE CIRCLED DIGIT SIX] +"\u277B" => "6" + +# ➅ [DINGBAT CIRCLED SANS-SERIF DIGIT SIX] +"\u2785" => "6" + +# ➏ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT SIX] +"\u278F" => "6" + +# 6 [FULLWIDTH DIGIT SIX] +"\uFF16" => "6" + +# ⒍ [DIGIT SIX FULL STOP] +"\u248D" => "6." + +# ⑹ [PARENTHESIZED DIGIT SIX] +"\u2479" => "(6)" + +# ⁷ [SUPERSCRIPT SEVEN] +"\u2077" => "7" + +# ₇ [SUBSCRIPT SEVEN] +"\u2087" => "7" + +# ⑦ [CIRCLED DIGIT SEVEN] +"\u2466" => "7" + +# ⓻ [DOUBLE CIRCLED DIGIT SEVEN] +"\u24FB" => "7" + +# ❼ [DINGBAT NEGATIVE CIRCLED DIGIT SEVEN] +"\u277C" => "7" + +# ➆ [DINGBAT CIRCLED SANS-SERIF DIGIT SEVEN] +"\u2786" => "7" + +# ➐ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT SEVEN] +"\u2790" => "7" + +# 7 [FULLWIDTH DIGIT SEVEN] +"\uFF17" => "7" + +# ⒎ [DIGIT SEVEN FULL STOP] +"\u248E" => "7." + +# ⑺ [PARENTHESIZED DIGIT SEVEN] +"\u247A" => "(7)" + +# ⁸ [SUPERSCRIPT EIGHT] +"\u2078" => "8" + +# ₈ [SUBSCRIPT EIGHT] +"\u2088" => "8" + +# ⑧ [CIRCLED DIGIT EIGHT] +"\u2467" => "8" + +# ⓼ [DOUBLE CIRCLED DIGIT EIGHT] +"\u24FC" => "8" + +# ❽ [DINGBAT NEGATIVE CIRCLED DIGIT EIGHT] +"\u277D" => "8" + +# ➇ [DINGBAT CIRCLED SANS-SERIF DIGIT EIGHT] +"\u2787" => "8" + +# ➑ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT EIGHT] +"\u2791" => "8" + +# 8 [FULLWIDTH DIGIT EIGHT] +"\uFF18" => "8" + +# ⒏ [DIGIT EIGHT FULL STOP] +"\u248F" => "8." + +# ⑻ [PARENTHESIZED DIGIT EIGHT] +"\u247B" => "(8)" + +# ⁹ [SUPERSCRIPT NINE] +"\u2079" => "9" + +# ₉ [SUBSCRIPT NINE] +"\u2089" => "9" + +# ⑨ [CIRCLED DIGIT NINE] +"\u2468" => "9" + +# ⓽ [DOUBLE CIRCLED DIGIT NINE] +"\u24FD" => "9" + +# ❾ [DINGBAT NEGATIVE CIRCLED DIGIT NINE] +"\u277E" => "9" + +# ➈ [DINGBAT CIRCLED SANS-SERIF DIGIT NINE] +"\u2788" => "9" + +# ➒ [DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT NINE] +"\u2792" => "9" + +# 9 [FULLWIDTH DIGIT NINE] +"\uFF19" => "9" + +# ⒐ [DIGIT NINE FULL STOP] +"\u2490" => "9." + +# ⑼ [PARENTHESIZED DIGIT NINE] +"\u247C" => "(9)" + +# ⑩ [CIRCLED NUMBER TEN] +"\u2469" => "10" + +# ⓾ [DOUBLE CIRCLED NUMBER TEN] +"\u24FE" => "10" + +# ❿ [DINGBAT NEGATIVE CIRCLED NUMBER TEN] +"\u277F" => "10" + +# ➉ [DINGBAT CIRCLED SANS-SERIF NUMBER TEN] +"\u2789" => "10" + +# ➓ [DINGBAT NEGATIVE CIRCLED SANS-SERIF NUMBER TEN] +"\u2793" => "10" + +# ⒑ [NUMBER TEN FULL STOP] +"\u2491" => "10." + +# ⑽ [PARENTHESIZED NUMBER TEN] +"\u247D" => "(10)" + +# ⑪ [CIRCLED NUMBER ELEVEN] +"\u246A" => "11" + +# ⓫ [NEGATIVE CIRCLED NUMBER ELEVEN] +"\u24EB" => "11" + +# ⒒ [NUMBER ELEVEN FULL STOP] +"\u2492" => "11." + +# ⑾ [PARENTHESIZED NUMBER ELEVEN] +"\u247E" => "(11)" + +# ⑫ [CIRCLED NUMBER TWELVE] +"\u246B" => "12" + +# ⓬ [NEGATIVE CIRCLED NUMBER TWELVE] +"\u24EC" => "12" + +# ⒓ [NUMBER TWELVE FULL STOP] +"\u2493" => "12." + +# ⑿ [PARENTHESIZED NUMBER TWELVE] +"\u247F" => "(12)" + +# ⑬ [CIRCLED NUMBER THIRTEEN] +"\u246C" => "13" + +# ⓭ [NEGATIVE CIRCLED NUMBER THIRTEEN] +"\u24ED" => "13" + +# ⒔ [NUMBER THIRTEEN FULL STOP] +"\u2494" => "13." + +# ⒀ [PARENTHESIZED NUMBER THIRTEEN] +"\u2480" => "(13)" + +# ⑭ [CIRCLED NUMBER FOURTEEN] +"\u246D" => "14" + +# ⓮ [NEGATIVE CIRCLED NUMBER FOURTEEN] +"\u24EE" => "14" + +# ⒕ [NUMBER FOURTEEN FULL STOP] +"\u2495" => "14." + +# ⒁ [PARENTHESIZED NUMBER FOURTEEN] +"\u2481" => "(14)" + +# ⑮ [CIRCLED NUMBER FIFTEEN] +"\u246E" => "15" + +# ⓯ [NEGATIVE CIRCLED NUMBER FIFTEEN] +"\u24EF" => "15" + +# ⒖ [NUMBER FIFTEEN FULL STOP] +"\u2496" => "15." + +# ⒂ [PARENTHESIZED NUMBER FIFTEEN] +"\u2482" => "(15)" + +# ⑯ [CIRCLED NUMBER SIXTEEN] +"\u246F" => "16" + +# ⓰ [NEGATIVE CIRCLED NUMBER SIXTEEN] +"\u24F0" => "16" + +# ⒗ [NUMBER SIXTEEN FULL STOP] +"\u2497" => "16." + +# ⒃ [PARENTHESIZED NUMBER SIXTEEN] +"\u2483" => "(16)" + +# ⑰ [CIRCLED NUMBER SEVENTEEN] +"\u2470" => "17" + +# ⓱ [NEGATIVE CIRCLED NUMBER SEVENTEEN] +"\u24F1" => "17" + +# ⒘ [NUMBER SEVENTEEN FULL STOP] +"\u2498" => "17." + +# ⒄ [PARENTHESIZED NUMBER SEVENTEEN] +"\u2484" => "(17)" + +# ⑱ [CIRCLED NUMBER EIGHTEEN] +"\u2471" => "18" + +# ⓲ [NEGATIVE CIRCLED NUMBER EIGHTEEN] +"\u24F2" => "18" + +# ⒙ [NUMBER EIGHTEEN FULL STOP] +"\u2499" => "18." + +# ⒅ [PARENTHESIZED NUMBER EIGHTEEN] +"\u2485" => "(18)" + +# ⑲ [CIRCLED NUMBER NINETEEN] +"\u2472" => "19" + +# ⓳ [NEGATIVE CIRCLED NUMBER NINETEEN] +"\u24F3" => "19" + +# ⒚ [NUMBER NINETEEN FULL STOP] +"\u249A" => "19." + +# ⒆ [PARENTHESIZED NUMBER NINETEEN] +"\u2486" => "(19)" + +# ⑳ [CIRCLED NUMBER TWENTY] +"\u2473" => "20" + +# ⓴ [NEGATIVE CIRCLED NUMBER TWENTY] +"\u24F4" => "20" + +# ⒛ [NUMBER TWENTY FULL STOP] +"\u249B" => "20." + +# ⒇ [PARENTHESIZED NUMBER TWENTY] +"\u2487" => "(20)" + +# « [LEFT-POINTING DOUBLE ANGLE QUOTATION MARK] +"\u00AB" => "\"" + +# » [RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK] +"\u00BB" => "\"" + +# “ [LEFT DOUBLE QUOTATION MARK] +"\u201C" => "\"" + +# ” [RIGHT DOUBLE QUOTATION MARK] +"\u201D" => "\"" + +# „ [DOUBLE LOW-9 QUOTATION MARK] +"\u201E" => "\"" + +# ″ [DOUBLE PRIME] +"\u2033" => "\"" + +# ‶ [REVERSED DOUBLE PRIME] +"\u2036" => "\"" + +# ❝ [HEAVY DOUBLE TURNED COMMA QUOTATION MARK ORNAMENT] +"\u275D" => "\"" + +# ❞ [HEAVY DOUBLE COMMA QUOTATION MARK ORNAMENT] +"\u275E" => "\"" + +# ❮ [HEAVY LEFT-POINTING ANGLE QUOTATION MARK ORNAMENT] +"\u276E" => "\"" + +# ❯ [HEAVY RIGHT-POINTING ANGLE QUOTATION MARK ORNAMENT] +"\u276F" => "\"" + +# " [FULLWIDTH QUOTATION MARK] +"\uFF02" => "\"" + +# ‘ [LEFT SINGLE QUOTATION MARK] +"\u2018" => "\'" + +# ’ [RIGHT SINGLE QUOTATION MARK] +"\u2019" => "\'" + +# ‚ [SINGLE LOW-9 QUOTATION MARK] +"\u201A" => "\'" + +# ‛ [SINGLE HIGH-REVERSED-9 QUOTATION MARK] +"\u201B" => "\'" + +# ′ [PRIME] +"\u2032" => "\'" + +# ‵ [REVERSED PRIME] +"\u2035" => "\'" + +# ‹ [SINGLE LEFT-POINTING ANGLE QUOTATION MARK] +"\u2039" => "\'" + +# › [SINGLE RIGHT-POINTING ANGLE QUOTATION MARK] +"\u203A" => "\'" + +# ❛ [HEAVY SINGLE TURNED COMMA QUOTATION MARK ORNAMENT] +"\u275B" => "\'" + +# ❜ [HEAVY SINGLE COMMA QUOTATION MARK ORNAMENT] +"\u275C" => "\'" + +# ' [FULLWIDTH APOSTROPHE] +"\uFF07" => "\'" + +# ‐ [HYPHEN] +"\u2010" => "-" + +# ‑ [NON-BREAKING HYPHEN] +"\u2011" => "-" + +# ‒ [FIGURE DASH] +"\u2012" => "-" + +# – [EN DASH] +"\u2013" => "-" + +# — [EM DASH] +"\u2014" => "-" + +# ⁻ [SUPERSCRIPT MINUS] +"\u207B" => "-" + +# ₋ [SUBSCRIPT MINUS] +"\u208B" => "-" + +# - [FULLWIDTH HYPHEN-MINUS] +"\uFF0D" => "-" + +# ⁅ [LEFT SQUARE BRACKET WITH QUILL] +"\u2045" => "[" + +# ❲ [LIGHT LEFT TORTOISE SHELL BRACKET ORNAMENT] +"\u2772" => "[" + +# [ [FULLWIDTH LEFT SQUARE BRACKET] +"\uFF3B" => "[" + +# ⁆ [RIGHT SQUARE BRACKET WITH QUILL] +"\u2046" => "]" + +# ❳ [LIGHT RIGHT TORTOISE SHELL BRACKET ORNAMENT] +"\u2773" => "]" + +# ] [FULLWIDTH RIGHT SQUARE BRACKET] +"\uFF3D" => "]" + +# ⁽ [SUPERSCRIPT LEFT PARENTHESIS] +"\u207D" => "(" + +# ₍ [SUBSCRIPT LEFT PARENTHESIS] +"\u208D" => "(" + +# ❨ [MEDIUM LEFT PARENTHESIS ORNAMENT] +"\u2768" => "(" + +# ❪ [MEDIUM FLATTENED LEFT PARENTHESIS ORNAMENT] +"\u276A" => "(" + +# ( [FULLWIDTH LEFT PARENTHESIS] +"\uFF08" => "(" + +# ⸨ [LEFT DOUBLE PARENTHESIS] +"\u2E28" => "((" + +# ⁾ [SUPERSCRIPT RIGHT PARENTHESIS] +"\u207E" => ")" + +# ₎ [SUBSCRIPT RIGHT PARENTHESIS] +"\u208E" => ")" + +# ❩ [MEDIUM RIGHT PARENTHESIS ORNAMENT] +"\u2769" => ")" + +# ❫ [MEDIUM FLATTENED RIGHT PARENTHESIS ORNAMENT] +"\u276B" => ")" + +# ) [FULLWIDTH RIGHT PARENTHESIS] +"\uFF09" => ")" + +# ⸩ [RIGHT DOUBLE PARENTHESIS] +"\u2E29" => "))" + +# ❬ [MEDIUM LEFT-POINTING ANGLE BRACKET ORNAMENT] +"\u276C" => "<" + +# ❰ [HEAVY LEFT-POINTING ANGLE BRACKET ORNAMENT] +"\u2770" => "<" + +# < [FULLWIDTH LESS-THAN SIGN] +"\uFF1C" => "<" + +# ❭ [MEDIUM RIGHT-POINTING ANGLE BRACKET ORNAMENT] +"\u276D" => ">" + +# ❱ [HEAVY RIGHT-POINTING ANGLE BRACKET ORNAMENT] +"\u2771" => ">" + +# > [FULLWIDTH GREATER-THAN SIGN] +"\uFF1E" => ">" + +# ❴ [MEDIUM LEFT CURLY BRACKET ORNAMENT] +"\u2774" => "{" + +# { [FULLWIDTH LEFT CURLY BRACKET] +"\uFF5B" => "{" + +# ❵ [MEDIUM RIGHT CURLY BRACKET ORNAMENT] +"\u2775" => "}" + +# } [FULLWIDTH RIGHT CURLY BRACKET] +"\uFF5D" => "}" + +# ⁺ [SUPERSCRIPT PLUS SIGN] +"\u207A" => "+" + +# ₊ [SUBSCRIPT PLUS SIGN] +"\u208A" => "+" + +# + [FULLWIDTH PLUS SIGN] +"\uFF0B" => "+" + +# ⁼ [SUPERSCRIPT EQUALS SIGN] +"\u207C" => "=" + +# ₌ [SUBSCRIPT EQUALS SIGN] +"\u208C" => "=" + +# = [FULLWIDTH EQUALS SIGN] +"\uFF1D" => "=" + +# ! [FULLWIDTH EXCLAMATION MARK] +"\uFF01" => "!" + +# ‼ [DOUBLE EXCLAMATION MARK] +"\u203C" => "!!" + +# ⁉ [EXCLAMATION QUESTION MARK] +"\u2049" => "!?" + +# # [FULLWIDTH NUMBER SIGN] +"\uFF03" => "#" + +# $ [FULLWIDTH DOLLAR SIGN] +"\uFF04" => "$" + +# ⁒ [COMMERCIAL MINUS SIGN] +"\u2052" => "%" + +# % [FULLWIDTH PERCENT SIGN] +"\uFF05" => "%" + +# & [FULLWIDTH AMPERSAND] +"\uFF06" => "&" + +# ⁎ [LOW ASTERISK] +"\u204E" => "*" + +# * [FULLWIDTH ASTERISK] +"\uFF0A" => "*" + +# , [FULLWIDTH COMMA] +"\uFF0C" => "," + +# . [FULLWIDTH FULL STOP] +"\uFF0E" => "." + +# ⁄ [FRACTION SLASH] +"\u2044" => "/" + +# / [FULLWIDTH SOLIDUS] +"\uFF0F" => "/" + +# : [FULLWIDTH COLON] +"\uFF1A" => ":" + +# ⁏ [REVERSED SEMICOLON] +"\u204F" => ";" + +# ; [FULLWIDTH SEMICOLON] +"\uFF1B" => ";" + +# ? [FULLWIDTH QUESTION MARK] +"\uFF1F" => "?" + +# ⁇ [DOUBLE QUESTION MARK] +"\u2047" => "??" + +# ⁈ [QUESTION EXCLAMATION MARK] +"\u2048" => "?!" + +# @ [FULLWIDTH COMMERCIAL AT] +"\uFF20" => "@" + +# \ [FULLWIDTH REVERSE SOLIDUS] +"\uFF3C" => "\\" + +# ‸ [CARET] +"\u2038" => "^" + +# ^ [FULLWIDTH CIRCUMFLEX ACCENT] +"\uFF3E" => "^" + +# _ [FULLWIDTH LOW LINE] +"\uFF3F" => "_" + +# ⁓ [SWUNG DASH] +"\u2053" => "~" + +# ~ [FULLWIDTH TILDE] +"\uFF5E" => "~" + +################################################################ +# Below is the Perl script used to generate the above mappings # +# from ASCIIFoldingFilter.java: # +################################################################ +# +# #!/usr/bin/perl +# +# use warnings; +# use strict; +# +# my @source_chars = (); +# my @source_char_descriptions = (); +# my $target = ''; +# +# while (<>) { +# if (/case\s+'(\\u[A-F0-9]+)':\s*\/\/\s*(.*)/i) { +# push @source_chars, $1; +# push @source_char_descriptions, $2; +# next; +# } +# if (/output\[[^\]]+\]\s*=\s*'(\\'|\\\\|.)'/) { +# $target .= $1; +# next; +# } +# if (/break;/) { +# $target = "\\\"" if ($target eq '"'); +# for my $source_char_num (0..$#source_chars) { +# print "# $source_char_descriptions[$source_char_num]\n"; +# print "\"$source_chars[$source_char_num]\" => \"$target\"\n\n"; +# } +# @source_chars = (); +# @source_char_descriptions = (); +# $target = ''; +# } +# } diff --git a/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/mapping-ISOLatin1Accent.txt b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/mapping-ISOLatin1Accent.txt new file mode 100644 index 0000000000..ede7742581 --- /dev/null +++ b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/mapping-ISOLatin1Accent.txt @@ -0,0 +1,246 @@ +# The ASF licenses this file to You 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. + +# Syntax: +# "source" => "target" +# "source".length() > 0 (source cannot be empty.) +# "target".length() >= 0 (target can be empty.) + +# example: +# "À" => "A" +# "\u00C0" => "A" +# "\u00C0" => "\u0041" +# "ß" => "ss" +# "\t" => " " +# "\n" => "" + +# À => A +"\u00C0" => "A" + +# Á => A +"\u00C1" => "A" + +#  => A +"\u00C2" => "A" + +# à => A +"\u00C3" => "A" + +# Ä => A +"\u00C4" => "A" + +# Å => A +"\u00C5" => "A" + +# Æ => AE +"\u00C6" => "AE" + +# Ç => C +"\u00C7" => "C" + +# È => E +"\u00C8" => "E" + +# É => E +"\u00C9" => "E" + +# Ê => E +"\u00CA" => "E" + +# Ë => E +"\u00CB" => "E" + +# Ì => I +"\u00CC" => "I" + +# Í => I +"\u00CD" => "I" + +# Î => I +"\u00CE" => "I" + +# Ï => I +"\u00CF" => "I" + +# IJ => IJ +"\u0132" => "IJ" + +# Ð => D +"\u00D0" => "D" + +# Ñ => N +"\u00D1" => "N" + +# Ò => O +"\u00D2" => "O" + +# Ó => O +"\u00D3" => "O" + +# Ô => O +"\u00D4" => "O" + +# Õ => O +"\u00D5" => "O" + +# Ö => O +"\u00D6" => "O" + +# Ø => O +"\u00D8" => "O" + +# Œ => OE +"\u0152" => "OE" + +# Þ +"\u00DE" => "TH" + +# Ù => U +"\u00D9" => "U" + +# Ú => U +"\u00DA" => "U" + +# Û => U +"\u00DB" => "U" + +# Ü => U +"\u00DC" => "U" + +# Ý => Y +"\u00DD" => "Y" + +# Ÿ => Y +"\u0178" => "Y" + +# à => a +"\u00E0" => "a" + +# á => a +"\u00E1" => "a" + +# â => a +"\u00E2" => "a" + +# ã => a +"\u00E3" => "a" + +# ä => a +"\u00E4" => "a" + +# å => a +"\u00E5" => "a" + +# æ => ae +"\u00E6" => "ae" + +# ç => c +"\u00E7" => "c" + +# è => e +"\u00E8" => "e" + +# é => e +"\u00E9" => "e" + +# ê => e +"\u00EA" => "e" + +# ë => e +"\u00EB" => "e" + +# ì => i +"\u00EC" => "i" + +# í => i +"\u00ED" => "i" + +# î => i +"\u00EE" => "i" + +# ï => i +"\u00EF" => "i" + +# ij => ij +"\u0133" => "ij" + +# ð => d +"\u00F0" => "d" + +# ñ => n +"\u00F1" => "n" + +# ò => o +"\u00F2" => "o" + +# ó => o +"\u00F3" => "o" + +# ô => o +"\u00F4" => "o" + +# õ => o +"\u00F5" => "o" + +# ö => o +"\u00F6" => "o" + +# ø => o +"\u00F8" => "o" + +# œ => oe +"\u0153" => "oe" + +# ß => ss +"\u00DF" => "ss" + +# þ => th +"\u00FE" => "th" + +# ù => u +"\u00F9" => "u" + +# ú => u +"\u00FA" => "u" + +# û => u +"\u00FB" => "u" + +# ü => u +"\u00FC" => "u" + +# ý => y +"\u00FD" => "y" + +# ÿ => y +"\u00FF" => "y" + +# ff => ff +"\uFB00" => "ff" + +# fi => fi +"\uFB01" => "fi" + +# fl => fl +"\uFB02" => "fl" + +# ffi => ffi +"\uFB03" => "ffi" + +# ffl => ffl +"\uFB04" => "ffl" + +# ſt => ft +"\uFB05" => "ft" + +# st => st +"\uFB06" => "st" diff --git a/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/protwords.txt b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/protwords.txt new file mode 100644 index 0000000000..1dfc0abecb --- /dev/null +++ b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/protwords.txt @@ -0,0 +1,21 @@ +# The ASF licenses this file to You 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. + +#----------------------------------------------------------------------- +# Use a protected word file to protect against the stemmer reducing two +# unrelated words to the same base word. + +# Some non-words that normally won't be encountered, +# just to test that they won't be stemmed. +dontstems +zwhacky + diff --git a/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/schema.xml b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/schema.xml new file mode 100644 index 0000000000..30300c0eb9 --- /dev/null +++ b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/schema.xml @@ -0,0 +1,672 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id + + + text + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/scripts.conf b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/scripts.conf new file mode 100644 index 0000000000..7be01a6bee --- /dev/null +++ b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/scripts.conf @@ -0,0 +1,24 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +user= +solr_hostname=localhost +solr_port=9293 +rsyncd_port=19293 +data_dir= +webapp_name=solr +master_host= +master_data_dir= +master_status_dir= diff --git a/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/solrconfig.xml b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/solrconfig.xml new file mode 100644 index 0000000000..052ddce699 --- /dev/null +++ b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/solrconfig.xml @@ -0,0 +1,1793 @@ + + + + + + + + + LUCENE_40 + + + + + + + + + + + + + + + + + + + + + + + + ${solr.data.dir:} + + + + + + + + + + + + + 1000 + + + false + + + 32 + 200 + + + + + + + + + 10 + + + + + + native + + + false + + + + + + true + + + + + + + 1 + + 0 + + + + + + false + + + + + + + + + + + + + + + + 15000 + true + + + + + + + + + + + + + + + + + + + + + + + + 1024 + + + + + + + + + + + + + + + + + + + + + + true + + + + + + 5 + + + 16 + + + + + + + + + + + + static firstSearcher warming in solrconfig.xml + + + + + + false + + + 2 + + + + + + + + + + + + + + + + + + + + + + + explicit + 10 + text + + + + + + + + + + + + + + explicit + json + true + text + + + + + + + + true + json + true + + + + + + + + explicit + + + velocity + browse + layout + Solritas + + + edismax + + text^0.5 features^1.0 name^1.2 sku^1.5 id^10.0 manu^1.1 cat^1.4 + title^10.0 description^5.0 keywords^5.0 author^2.0 resourcename^1.0 + + text + 100% + *:* + 10 + *,score + + + text^0.5 features^1.0 name^1.2 sku^1.5 id^10.0 manu^1.1 cat^1.4 + title^10.0 description^5.0 keywords^5.0 author^2.0 resourcename^1.0 + + text,features,name,sku,id,manu,cat,title,description,keywords,author,resourcename + 3 + + + + + + on + text content file_name + html + 0 + file_name + + <b> + </b> + 0 + title + + 3 + 200 + content + 750 + + + + + + + + + spellcheck + + + + + + + + + + + + + + + + + application/json + + + + + application/csv + + + + + + + true + ignored_ + + + true + links + ignored_ + + + + + + + + + + + + + + + + + + + + + + + + search + solrpingquery + + + all + + + + + + + + + explicit + true + + + + + + + + + + + + + + + + textSpell + + + + + + default + name + solr.DirectSolrSpellChecker + + internal + + 0.5 + + 2 + + 1 + + 5 + + 4 + + 0.01 + + + + + + wordbreak + solr.WordBreakSolrSpellChecker + name + true + true + 10 + + + + + + + + + + + + + + + + text + + default + wordbreak + on + true + 10 + 5 + 5 + true + true + 10 + 5 + + + spellcheck + + + + + + + + + + text + true + + + tvComponent + + + + + + + + + default + + + org.carrot2.clustering.lingo.LingoClusteringAlgorithm + + + 20 + + + clustering/carrot2 + + + ENGLISH + + + stc + org.carrot2.clustering.stc.STCClusteringAlgorithm + + + + + + + true + default + true + + name + id + + features + + true + + + + false + + edismax + + text^0.5 features^1.0 name^1.2 sku^1.5 id^10.0 manu^1.1 cat^1.4 + + *:* + 10 + *,score + + + clustering + + + + + + + + + + true + false + + + terms + + + + + + + + string + elevate.xml + + + + + + explicit + text + + + elevator + + + + + + + + + + + 100 + + + + + + + + 70 + + 0.5 + + [-\w ,/\n\"']{20,200} + + + + + + + ]]> + ]]> + + + + + + + + + + + + + + + + + + + + + + + + ,, + ,, + ,, + ,, + ,]]> + ]]> + + + + + + 10 + .,!? + + + + + + + WORD + + + en + US + + + + + + + + + + + + + + + + + + + + + + text/plain; charset=UTF-8 + + + + + + + + + 5 + + + + + + + + + + + + + + + + + + *:* + + + diff --git a/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/spellings.txt b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/spellings.txt new file mode 100644 index 0000000000..162a044d56 --- /dev/null +++ b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/spellings.txt @@ -0,0 +1,2 @@ +pizza +history diff --git a/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/stopwords.txt b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/stopwords.txt new file mode 100644 index 0000000000..ae1e83eeb3 --- /dev/null +++ b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/stopwords.txt @@ -0,0 +1,14 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. diff --git a/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/stopwords_en.txt b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/stopwords_en.txt new file mode 100644 index 0000000000..2c164c0b2a --- /dev/null +++ b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/stopwords_en.txt @@ -0,0 +1,54 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +# a couple of test stopwords to test that the words are really being +# configured from this file: +stopworda +stopwordb + +# Standard english stop words taken from Lucene's StopAnalyzer +a +an +and +are +as +at +be +but +by +for +if +in +into +is +it +no +not +of +on +or +such +that +the +their +then +there +these +they +this +to +was +will +with diff --git a/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/synonyms.txt b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/synonyms.txt new file mode 100644 index 0000000000..5c3b95fb6a --- /dev/null +++ b/KeywordSearch/release/solr/solr/configsets/AutopsyConfig/conf/synonyms.txt @@ -0,0 +1,31 @@ +# The ASF licenses this file to You 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. + +#----------------------------------------------------------------------- +#some test synonym mappings unlikely to appear in real input text +aaafoo => aaabar +bbbfoo => bbbfoo bbbbar +cccfoo => cccbar cccbaz +fooaaa,baraaa,bazaaa + +# Some synonym groups specific to this example +GB,gib,gigabyte,gigabytes +MB,mib,megabyte,megabytes +Television, Televisions, TV, TVs +#notice we use "gib" instead of "GiB" so any WordDelimiterFilter coming +#after us won't split it into two words. + +# Synonym mappings can be used for spelling correction too +pixima => pixma + +a\,a => b\,b + diff --git a/KeywordSearch/release/solr/solr/solr.xml b/KeywordSearch/release/solr/solr/solr.xml index f2d49d693e..94d60b6a29 100644 --- a/KeywordSearch/release/solr/solr/solr.xml +++ b/KeywordSearch/release/solr/solr/solr.xml @@ -17,17 +17,29 @@ --> - + This is an example of a simple "solr.xml" file for configuring one or + more Solr Cores, as well as allowing Cores to be added, removed, and + reloaded via HTTP requests. + + More information about options available in this configuration file, + and Solr Core administration can be found online: + http://wiki.apache.org/solr/CoreAdmin +--> + + + + + ${host:} + ${jetty.port:8983} + ${hostContext:solr} + ${zkClientTimeout:30000} + ${genericCoreNodeNames:true} + + + + ${socketTimeout:0} + ${connTimeout:0} + - - - diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AbstractFileChunk.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AbstractFileChunk.java index 8f4aedd0f0..7f82fa5688 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AbstractFileChunk.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AbstractFileChunk.java @@ -50,22 +50,51 @@ class AbstractFileChunk { * * @return */ - public String getIdString() { + String getIdString() { return Server.getChunkIdString(this.parent.getSourceFile().getId(), this.chunkID); } - public boolean index(Ingester ingester, byte[] content, long contentSize, Charset indexCharset) throws IngesterException { - boolean success = true; - ByteContentStream bcs = new ByteContentStream(content, contentSize, parent.getSourceFile(), indexCharset); + void index(Ingester ingester, byte[] content, long contentSize, Charset indexCharset) throws IngesterException { + // We are currently only passing utf-8 as indexCharset. If other charsets were to be used in the future, + // this might need to be changed to accommodate. + byte[] saitizedContent = sanitize(content, indexCharset); + ByteContentStream bcs = new ByteContentStream(saitizedContent, contentSize, parent.getSourceFile(), indexCharset); try { ingester.ingest(this, bcs, content.length); - //logger.log(Level.INFO, "Ingesting string chunk: " + this.getName() + ": " + chunkID); } catch (Exception ingEx) { - success = false; throw new IngesterException(NbBundle.getMessage(this.getClass(), "AbstractFileChunk.index.exception.msg", parent.getSourceFile().getId(), chunkID), ingEx); } - return success; + } + + // Given a byte array, filter out all occurances non-characters + // http://unicode.org/cldr/utility/list-unicodeset.jsp?a=[:Noncharacter_Code_Point=True:] + // and non-printable control characters except tabulator, new line and carriage return + // and replace them with the character (^) + private static byte[] sanitize(byte[] input, Charset indexCharset) { + String inputString = new String(input, indexCharset); + StringBuilder sanitized = new StringBuilder(inputString.length()); + char ch; + for (int i = 0; i < inputString.length(); i++) { + ch = inputString.charAt(i); + if (charIsValidSolrUTF8(ch)) { + sanitized.append(ch); + } else { + sanitized.append('^'); // NON-NLS + } + } + + byte[] output = sanitized.toString().getBytes(indexCharset); + return output; + } + + // Is the given character a valid UTF-8 character + // return true if it is, false otherwise + private static boolean charIsValidSolrUTF8(char ch) { + return (ch % 0x10000 != 0xffff && // 0xffff - 0x10ffff range step 0x10000 + ch % 0x10000 != 0xfffe && // 0xfffe - 0x10fffe range + (ch <= 0xfdd0 || ch >= 0xfdef) && // 0xfdd0 - 0xfdef + (ch > 0x1F || ch == 0x9 || ch == 0xa || ch == 0xd)); } } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties index 7c1dc087cb..62a15c1517 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties @@ -61,11 +61,12 @@ AbstractKeywordSearchPerformer.search.noFilesIdxdMsg=No files were indexed ExtractedContentPanel.setMarkup.panelTxt=Loading text... Please wait ExtractedContentViewer.toString=Extracted Text ExtractedContentViewer.toolTip=Displays extracted text from files and keyword-search results. Requires Keyword Search ingest to be run on a file to activate this viewer. -ExtractedContentViewer.getTitle=Text +ExtractedContentViewer.getTitle=Indexed Text ExtractedContentViewer.getSolrContent.knownFileMsg=

{0} is a known file (based on MD5 hash) and does not have text in the index.

ExtractedContentViewer.getSolrContent.noTxtYetMsg=

{0} does not have text in the index.
It may have no text, not been analyzed yet, or keyword search was not enabled during ingest.

ExtractedContentViewer.getSolrContent.txtBodyItal={0} HighlightedMatchesSource.getMarkup.noMatchMsg=
There were no keyword hits on this page. 
Keyword could have been in file name.
Advance to another page for hits or choose Extracted Text to view original text..
+HighlightedMatchesSource.getMarkup.queryFailedMsg=
Failed to retrieve keyword hit results. 
Confirm that Autopsy can connect to the Solr server.
HighlightedMatchesSource.toString=Search Results Installer.reportPortError=Indexing server port {0} is not available. Check if your security software does not block {1} and consider changing {2} in {3} property file in the application user folder. Then try rebooting your system if another process was causing the conflict. Installer.reportStopPortError=Indexing server stop port {0} is not available. Consider changing {1} in {2} property file in the application user folder. @@ -213,14 +214,16 @@ Server.openCore.exception.alreadyOpen.msg=Already an open Core\! Explicitely clo Server.queryNumIdxFiles.exception.msg=Error querying number of indexed files, Server.queryNumIdxChunks.exception.msg=Error querying number of indexed chunks, Server.queryNumIdxDocs.exception.msg=Error querying number of indexed documents, -Server.queryIsIdxd.exception.msg=Error checkign if content is indexed, +Server.queryIsIdxd.exception.msg=Error checking if content is indexed, 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.openCore.exception.msg=Core open requested, but server not yet running Server.openCore.exception.cantOpen.msg=Could not open Core Server.openCore.exception.cantOpen.msg2=Could not open Core +Server.openCore.exception.cantOpenForCase.msg=Could not create keyword search index for case {0} Server.request.exception.exception.msg=Could not issue Solr request Server.commit.exception.msg=Could not commit index Server.addDoc.exception.msg=Could not add document to index via update handler\: {0} @@ -285,3 +288,7 @@ SearchRunner.Searcher.done.err.msg=Error performing keyword search KeywordSearchGlobalSearchSettingsPanel.timeRadioButton5.toolTipText=Fastest overall, but no results until the end KeywordSearchGlobalSearchSettingsPanel.timeRadioButton5.text=No periodic searches KeywordSearchIngestModule.startUp.fileTypeDetectorInitializationException.msg=Error initializing the file type detector. +SolrConnectionCheck.HostnameOrPort=Invalid hostname and/or port number. +SolrConnectionCheck.Hostname=Invalid hostname. +SolrConnectionCheck.Port=Invalid port number. +SolrConnectionCheck.MissingHostname=Missing hostname. diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/DropdownListSearchPanel.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/DropdownListSearchPanel.java index d106712c93..1d91a78dd3 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/DropdownListSearchPanel.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/DropdownListSearchPanel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2014 Basis Technology Corp. + * Copyright 2011-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -39,7 +39,6 @@ import org.openide.util.NbBundle; import org.openide.util.actions.SystemAction; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.ingest.IngestManager; -import org.sleuthkit.autopsy.ingest.IngestManager.IngestJobEvent; /** * Viewer panel widget for keyword lists that is used in the ingest config and @@ -50,8 +49,8 @@ class DropdownListSearchPanel extends KeywordSearchPanel { private static final Logger logger = Logger.getLogger(DropdownListSearchPanel.class.getName()); private static DropdownListSearchPanel instance; private XmlKeywordSearchList loader; - private KeywordListsTableModel listsTableModel; - private KeywordsTableModel keywordsTableModel; + private final KeywordListsTableModel listsTableModel; + private final KeywordsTableModel keywordsTableModel; private ActionListener searchAddListener; private boolean ingestRunning; @@ -119,10 +118,8 @@ class DropdownListSearchPanel extends KeywordSearchPanel { IngestManager.getInstance().addIngestJobEventListener(new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { - String changed = evt.getPropertyName(); - if (changed.equals(IngestJobEvent.STARTED.toString()) - || changed.equals(IngestJobEvent.COMPLETED.toString()) - || changed.equals(IngestJobEvent.CANCELLED.toString())) { + Object source = evt.getSource(); + if (source instanceof String && ((String) source).equals("LOCAL")) { EventQueue.invokeLater(new Runnable() { @Override public void run() { diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/DropdownToolbar.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/DropdownToolbar.java index 8b81623f0e..7c24b99337 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/DropdownToolbar.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/DropdownToolbar.java @@ -29,17 +29,16 @@ import java.awt.event.MouseEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.logging.Level; - import org.sleuthkit.autopsy.coreutils.Logger; import javax.swing.SwingUtilities; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.core.RuntimeProperties; /** - * Keyword search toolbar (in upper right, by default) which allows to search - * for single terms or phrases - * + * Keyword search tool bar (in upper right, by default) with drop down panels + * for ad hoc searches by list or expression. */ class DropdownToolbar extends javax.swing.JPanel { @@ -49,17 +48,11 @@ class DropdownToolbar extends javax.swing.JPanel { private static DropdownToolbar instance; private DropdownSingleTermSearchPanel dropPanel = null; - /** - * Creates new form DropdownToolbar - */ private DropdownToolbar() { initComponents(); customizeComponents(); } - /** - * @return the default instance DropdownToolbar - */ public synchronized static DropdownToolbar getDefault() { if (instance == null) { instance = new DropdownToolbar(); @@ -228,28 +221,20 @@ class DropdownToolbar extends javax.swing.JPanel { @Override public void propertyChange(PropertyChangeEvent evt) { String changed = evt.getPropertyName(); - Object oldValue = evt.getOldValue(); - Object newValue = evt.getNewValue(); - if (changed.equals(Case.Events.CURRENT_CASE.toString())) { dropPanel.resetSearchBox(); - if (newValue == null) { - setFields(false); - } else { - setFields(true); - } + setFields(null != evt.getNewValue() && RuntimeProperties.coreComponentsAreActive()); } else if (changed.equals(Server.CORE_EVT)) { - final Server.CORE_EVT_STATES state = (Server.CORE_EVT_STATES) newValue; + final Server.CORE_EVT_STATES state = (Server.CORE_EVT_STATES) evt.getNewValue(); switch (state) { case STARTED: try { final int numIndexedFiles = KeywordSearch.getServer().queryNumIndexedFiles(); - KeywordSearch.fireNumIndexedFilesChange(null, new Integer(numIndexedFiles)); - //setFilesIndexed(numIndexedFiles); + KeywordSearch.fireNumIndexedFilesChange(null, numIndexedFiles); } catch (NoOpenCoreException ex) { - logger.log(Level.SEVERE, "Error executing Solr query, " + ex); //NON-NLS + logger.log(Level.SEVERE, "Error executing Solr query, {0}", ex); //NON-NLS } catch (KeywordSearchModuleException se) { - logger.log(Level.SEVERE, "Error executing Solr query, " + se.getMessage()); //NON-NLS + logger.log(Level.SEVERE, "Error executing Solr query, {0}", se.getMessage()); //NON-NLS } break; case STOPPED: diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalEditListPanel.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalEditListPanel.java index c3b5426d82..87dc6b6f52 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalEditListPanel.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalEditListPanel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2014 Basis Technology Corp. + * Copyright 2011-2015 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -43,14 +43,13 @@ import javax.swing.table.AbstractTableModel; import javax.swing.table.TableColumn; import org.sleuthkit.autopsy.corecomponents.OptionsPanel; import org.sleuthkit.autopsy.ingest.IngestManager; -import org.sleuthkit.autopsy.ingest.IngestManager.IngestJobEvent; /** * GlobalEditListPanel widget to manage keywords in lists */ class GlobalEditListPanel extends javax.swing.JPanel implements ListSelectionListener, OptionsPanel { - private static Logger logger = Logger.getLogger(GlobalEditListPanel.class.getName()); + private static final Logger logger = Logger.getLogger(GlobalEditListPanel.class.getName()); private KeywordTableModel tableModel; private KeywordList currentKeywordList; @@ -129,10 +128,8 @@ class GlobalEditListPanel extends javax.swing.JPanel implements ListSelectionLis IngestManager.getInstance().addIngestJobEventListener(new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { - String changed = evt.getPropertyName(); - if (changed.equals(IngestJobEvent.STARTED.toString()) - || changed.equals(IngestJobEvent.COMPLETED.toString()) - || changed.equals(IngestJobEvent.CANCELLED.toString())) { + Object source = evt.getSource(); + if (source instanceof String && ((String) source).equals("LOCAL")) { EventQueue.invokeLater(new Runnable() { @Override public void run() { diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/HighlightedTextMarkup.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/HighlightedTextMarkup.java index ce994f57a9..2616e2a11b 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/HighlightedTextMarkup.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/HighlightedTextMarkup.java @@ -34,7 +34,6 @@ import org.apache.solr.client.solrj.response.QueryResponse; import org.sleuthkit.autopsy.coreutils.Version; import org.sleuthkit.autopsy.datamodel.TextMarkupLookup; import org.sleuthkit.autopsy.keywordsearch.KeywordQueryFilter.FilterType; -import org.sleuthkit.datamodel.Content; /** * Highlights hits for a given document. Knows about pages and such for the @@ -380,12 +379,8 @@ class HighlightedTextMarkup implements TextMarkup, TextMarkupLookup { return "
" + highlightedContent + "
"; //NON-NLS } - } catch (NoOpenCoreException ex) { - logger.log(Level.WARNING, "Couldn't query markup for page: " + currentPage, ex); //NON-NLS - return ""; - } catch (KeywordSearchModuleException ex) { - logger.log(Level.WARNING, "Could not query markup for page: " + currentPage, ex); //NON-NLS - return ""; + } catch (NoOpenCoreException | KeywordSearchModuleException ex) { + return NbBundle.getMessage(this.getClass(), "HighlightedMatchesSource.getMarkup.queryFailedMsg"); } } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Installer.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Installer.java index 99e57153ca..1008edaf56 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Installer.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Installer.java @@ -38,7 +38,6 @@ import org.sleuthkit.autopsy.coreutils.Version; class Installer extends ModuleInstall { private static final Logger logger = Logger.getLogger(Installer.class.getName()); - private final static int SERVER_START_RETRIES = 5; @Override public void restored() { @@ -48,133 +47,19 @@ class Installer extends ModuleInstall { Case.addPropertyChangeListener(new KeywordSearch.CaseChangeListener()); final Server server = KeywordSearch.getServer(); - int retries = SERVER_START_RETRIES; - - //TODO revise this logic, handle other server types, move some logic to Server class try { - //check if running from previous application instance and try to shut down - logger.log(Level.INFO, "Checking if server is running"); //NON-NLS - if (server.isRunning()) { - //TODO this could hang if other type of server is running - logger.log(Level.WARNING, "Already a server running on " + server.getCurrentSolrServerPort() //NON-NLS - + " port, maybe leftover from a previous run. Trying to shut it down."); //NON-NLS - //stop gracefully - server.stop(); - logger.log(Level.INFO, "Re-checking if server is running"); //NON-NLS - if (server.isRunning()) { - int serverPort = server.getCurrentSolrServerPort(); - int serverStopPort = server.getCurrentSolrStopPort(); - logger.log(Level.SEVERE, "There's already a server running on " //NON-NLS - + serverPort + " port that can't be shutdown."); //NON-NLS - if (!Server.isPortAvailable(serverPort)) { - reportPortError(serverPort); - } else if (!Server.isPortAvailable(serverStopPort)) { - reportStopPortError(serverStopPort); - } else { - //some other reason - reportInitError(); - } - - //in this case give up - } else { - logger.log(Level.INFO, "Old Solr server shutdown successfully."); //NON-NLS - //make sure there really isn't a hang Solr process, in case isRunning() reported false - server.killSolr(); - } - } - } catch (KeywordSearchModuleException e) { - logger.log(Level.SEVERE, "Starting server failed, will try to kill. ", e); //NON-NLS - server.killSolr(); - } - - try { - //Ensure no other process is still bound to that port, even if we think solr is not running - //Try to bind to the port 4 times at 1 second intervals. - //TODO move some of this logic to Server class - for (int i = 0; i <= 3; i++) { - logger.log(Level.INFO, "Checking if port available."); //NON-NLS - if (Server.isPortAvailable(server.getCurrentSolrServerPort())) { - logger.log(Level.INFO, "Port available, trying to start server."); //NON-NLS - server.start(); - break; - } else if (i == 3) { - logger.log(Level.INFO, "No port available, done retrying."); //NON-NLS - reportPortError(server.getCurrentSolrServerPort()); - retries = 0; - break; - } else { - try { - Thread.sleep(1000); - } catch (InterruptedException iex) { - logger.log(Level.WARNING, "Timer interrupted"); //NON-NLS - } - } - } - } catch (SolrServerNoPortException npe) { - logger.log(Level.SEVERE, "Starting server failed due to no port available. ", npe); //NON-NLS - //try to kill it - - } catch (KeywordSearchModuleException e) { - logger.log(Level.SEVERE, "Starting server failed. ", e); //NON-NLS - } - - //retry if needed - //TODO this loop may be now redundant - while (retries-- > 0) { - try { - Thread.sleep(1000); - } catch (InterruptedException ex) { - logger.log(Level.WARNING, "Timer interrupted."); //NON-NLS - } - - try { - logger.log(Level.INFO, "Ensuring the server is running, retries remaining: " + retries); //NON-NLS - if (!server.isRunning()) { - logger.log(Level.WARNING, "Server still not running"); //NON-NLS - try { - logger.log(Level.WARNING, "Trying to start the server. "); //NON-NLS - server.start(); - } catch (SolrServerNoPortException npe) { - logger.log(Level.SEVERE, "Starting server failed due to no port available. ", npe); //NON-NLS - } - } else { - logger.log(Level.INFO, "Server appears now running. "); //NON-NLS - break; - } - } catch (KeywordSearchModuleException ex) { - logger.log(Level.SEVERE, "Starting server failed. ", ex); //NON-NLS - //retry if has retries - } - - } //end of retry while loop - - //last check if still not running to report errors - try { - Thread.sleep(1000); - } catch (InterruptedException ex) { - logger.log(Level.WARNING, "Timer interrupted."); //NON-NLS - } - try { - logger.log(Level.INFO, "Last check if server is running. "); //NON-NLS - if (!server.isRunning()) { - logger.log(Level.SEVERE, "Server is still not running. "); //NON-NLS - //check if port is taken or some other reason - int serverPort = server.getCurrentSolrServerPort(); - int serverStopPort = server.getCurrentSolrStopPort(); - if (!Server.isPortAvailable(serverPort)) { - reportPortError(serverPort); - } else if (!Server.isPortAvailable(serverStopPort)) { - reportStopPortError(serverStopPort); - } else { - //some other reason - reportInitError(); - } + server.start(); + } catch (SolrServerNoPortException ex) { + logger.log(Level.SEVERE, "Failed to start Keyword Search server: ", ex); //NON-NLS + if (ex.getPortNumber() == server.getCurrentSolrServerPort()) { + reportPortError(ex.getPortNumber()); + } else { + reportStopPortError(ex.getPortNumber()); } } catch (KeywordSearchModuleException ex) { - logger.log(Level.SEVERE, "Starting server failed. ", ex); //NON-NLS - reportInitError(); + logger.log(Level.SEVERE, "Failed to start Keyword Search server: ", ex); //NON-NLS + reportInitError(ex.getMessage()); } - } @Override @@ -213,13 +98,10 @@ class Installer extends ModuleInstall { }); } - private void reportInitError() { + private void reportInitError(final String msg) { WindowManager.getDefault().invokeWhenUIReady(new Runnable() { @Override public void run() { - final String msg = NbBundle.getMessage(this.getClass(), "Installer.reportInitError", KeywordSearch.getServer().getCurrentSolrServerPort(), Version.getName(), Server.PROPERTIES_CURRENT_SERVER_PORT, Server.PROPERTIES_FILE); - MessageNotifyUtil.Notify.error(NbBundle.getMessage(this.getClass(), "Installer.errorInitKsmMsg"), msg); - MessageNotifyUtil.Notify.error(NbBundle.getMessage(this.getClass(), "Installer.errorInitKsmMsg"), msg); } }); diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordListsManager.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordListsManager.java index 13bf972e18..bdae07a971 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordListsManager.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordListsManager.java @@ -72,5 +72,12 @@ public class KeywordListsManager extends Observable { } return names; } + + /** + * Force reload of the keyword lists XML file. + */ + public static void reloadKeywordLists(){ + XmlKeywordSearchList.getCurrent().reload(); + } } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearch.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearch.java index e92b4f5410..627f2559f9 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearch.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearch.java @@ -23,15 +23,15 @@ import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.IOException; import java.util.logging.FileHandler; +import java.util.logging.Level; import java.util.logging.Logger; import java.util.logging.SimpleFormatter; import org.openide.util.Exceptions; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.PlatformUtil; import org.sleuthkit.autopsy.keywordsearch.KeywordSearchResultFactory.BlackboardResultWriter; -import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; -import java.util.logging.Level; /** * Wrapper over KeywordSearch Solr server singleton. The class also provides @@ -123,11 +123,16 @@ public class KeywordSearch { @Override public void propertyChange(PropertyChangeEvent evt) { String changed = evt.getPropertyName(); - Object oldValue = evt.getOldValue(); - Object newValue = evt.getNewValue(); final Logger logger = Logger.getLogger(CaseChangeListener.class.getName()); if (changed.equals(Case.Events.CURRENT_CASE.toString())) { + /* + * don't call getOld/NewValue() unless they are needed, since + * they might do expensive db operations to load transient + * values lost in serialization + */ + Object oldValue = evt.getOldValue(); + Object newValue = evt.getNewValue(); if (newValue != null) { // new case is open try { diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchIngestModule.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchIngestModule.java index 79be07e769..b79fdca397 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchIngestModule.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchIngestModule.java @@ -25,6 +25,8 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.StringExtract.StringExtractUnicodeTable.SCRIPT; @@ -35,6 +37,8 @@ import org.sleuthkit.autopsy.ingest.IngestMessage.MessageType; import org.sleuthkit.autopsy.ingest.IngestModuleReferenceCounter; import org.sleuthkit.autopsy.ingest.IngestServices; import org.sleuthkit.autopsy.keywordsearch.Ingester.IngesterException; +import org.sleuthkit.autopsy.keywordsearchservice.KeywordSearchService; +import org.sleuthkit.autopsy.keywordsearchservice.KeywordSearchServiceException; import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.TskCoreException; @@ -141,45 +145,71 @@ public final class KeywordSearchIngestModule implements FileIngestModule { // increment the module reference count // if first instance of this module for this job then check the server and existence of keywords if (refCounter.incrementAndGet(jobId) == 1) { - final Server server = KeywordSearch.getServer(); - try { - if (!server.isRunning()) { + if (Case.getCurrentCase().getCaseType() == Case.CaseType.MULTI_USER_CASE) { + // for multi-user cases need to verify connection to remore SOLR server + KeywordSearchService kwsService = new SolrSearchService(); + int port; + try { + port = Integer.parseInt(UserPreferences.getIndexingServerPort()); + } catch (NumberFormatException ex) { + // if there is an error parsing the port number + String msg = NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.init.badInitMsg"); + String details = NbBundle.getMessage(this.getClass(), "SolrConnectionCheck.Port"); + logger.log(Level.SEVERE, "{0}: {1} {2}", new Object[]{msg, details, ex.toString()}); + services.postMessage(IngestMessage.createErrorMessage(KeywordSearchModuleFactory.getModuleName(), msg, details)); + throw new IngestModuleException(msg); + } + try { + kwsService.tryConnect(UserPreferences.getIndexingServerHost(), port); + } catch (KeywordSearchServiceException ex) { + String msg = NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.init.badInitMsg"); + String details = ex.getMessage(); + logger.log(Level.SEVERE, "{0}: {1} {2}", new Object[]{msg, details, ex.toString()}); + services.postMessage(IngestMessage.createErrorMessage(KeywordSearchModuleFactory.getModuleName(), msg, details)); + throw new IngestModuleException(msg); + } + } else { + // for single-user cases need to verify connection to local SOLR service + final Server server = KeywordSearch.getServer(); + try { + if (!server.isRunning()) { + String msg = NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.init.badInitMsg"); + logger.log(Level.SEVERE, msg); + String details = NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.init.tryStopSolrMsg", msg); + services.postMessage(IngestMessage.createErrorMessage(KeywordSearchModuleFactory.getModuleName(), msg, details)); + throw new IngestModuleException(msg); + } + } catch (KeywordSearchModuleException ex) { + logger.log(Level.WARNING, "Error checking if Solr server is running while initializing ingest", ex); //NON-NLS + //this means Solr is not properly initialized String msg = NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.init.badInitMsg"); - logger.log(Level.SEVERE, msg); String details = NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.init.tryStopSolrMsg", msg); services.postMessage(IngestMessage.createErrorMessage(KeywordSearchModuleFactory.getModuleName(), msg, details)); throw new IngestModuleException(msg); } - } catch (KeywordSearchModuleException ex) { - logger.log(Level.WARNING, "Error checking if Solr server is running while initializing ingest", ex); //NON-NLS - //this means Solr is not properly initialized - String msg = NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.init.badInitMsg"); - String details = NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.init.tryStopSolrMsg", msg); - services.postMessage(IngestMessage.createErrorMessage(KeywordSearchModuleFactory.getModuleName(), msg, details)); - throw new IngestModuleException(msg); - } - try { - // make an actual query to verify that server is responding - // we had cases where getStatus was OK, but the connection resulted in a 404 - server.queryNumIndexedDocuments(); - } catch (KeywordSearchModuleException | NoOpenCoreException ex) { - throw new IngestModuleException( - NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.init.exception.errConnToSolr.msg", - ex.getMessage())); - } - - // check if this job has any searchable keywords - List keywordLists = XmlKeywordSearchList.getCurrent().getListsL(); - boolean hasKeywordsForSearch = false; - for (KeywordList keywordList : keywordLists) { - if (settings.keywordListIsEnabled(keywordList.getName()) && !keywordList.getKeywords().isEmpty()) { - hasKeywordsForSearch = true; - break; + try { + // make an actual query to verify that server is responding + // we had cases where getStatus was OK, but the connection resulted in a 404 + server.queryNumIndexedDocuments(); + } catch (KeywordSearchModuleException | NoOpenCoreException ex) { + throw new IngestModuleException( + NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.init.exception.errConnToSolr.msg", + ex.getMessage())); + } + + // check if this job has any searchable keywords + List keywordLists = XmlKeywordSearchList.getCurrent().getListsL(); + boolean hasKeywordsForSearch = false; + for (KeywordList keywordList : keywordLists) { + if (settings.keywordListIsEnabled(keywordList.getName()) && !keywordList.getKeywords().isEmpty()) { + hasKeywordsForSearch = true; + break; + } + } + if (!hasKeywordsForSearch) { + services.postMessage(IngestMessage.createWarningMessage(KeywordSearchModuleFactory.getModuleName(), NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.init.noKwInLstMsg"), + NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.init.onlyIdxKwSkipMsg"))); } - } - if (!hasKeywordsForSearch) { - services.postMessage(IngestMessage.createWarningMessage(KeywordSearchModuleFactory.getModuleName(), NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.init.noKwInLstMsg"), - NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.init.onlyIdxKwSkipMsg"))); } } @@ -245,7 +275,7 @@ public final class KeywordSearchIngestModule implements FileIngestModule { public void shutDown() { logger.log(Level.INFO, "Instance {0}", instanceNum); //NON-NLS - if (initialized == false) { + if ((initialized == false) || (context == null)) { return; } @@ -314,6 +344,9 @@ public final class KeywordSearchIngestModule implements FileIngestModule { synchronized (ingestStatus) { Map ingestStatusForJob = ingestStatus.get(jobId); + if (ingestStatusForJob == null) { + return; + } for (IngestStatus s : ingestStatusForJob.values()) { switch (s) { case TEXT_INGESTED: diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchOptionsPanelController.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchOptionsPanelController.java index 68fe399c3e..8deb9a7446 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchOptionsPanelController.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchOptionsPanelController.java @@ -32,7 +32,7 @@ import org.sleuthkit.autopsy.coreutils.Logger; @OptionsPanelController.TopLevelRegistration( categoryName = "#OptionsCategory_Name_KeywordSearchOptions", iconBase = "org/sleuthkit/autopsy/keywordsearch/options-icon.png", - position = 2, + position = 3, keywords = "#OptionsCategory_Keywords_KeywordSearchOptions", keywordsCategory = "KeywordSearchOptions") public final class KeywordSearchOptionsPanelController extends OptionsPanelController { diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/LuceneQuery.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/LuceneQuery.java index 6661294f72..1f395495c1 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/LuceneQuery.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/LuceneQuery.java @@ -33,14 +33,15 @@ import org.apache.solr.client.solrj.SolrRequest.METHOD; import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.common.SolrDocument; import org.apache.solr.common.SolrDocumentList; +import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.EscapeUtil; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.Version; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE; -import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskException; @@ -204,48 +205,50 @@ class LuceneQuery implements KeywordSearchQuery { final Server solrServer = KeywordSearch.getServer(); SolrQuery q = createAndConfigureSolrQuery(snippets); + QueryResponse response; + SolrDocumentList resultList; + Map>> highlightResponse; + Set uniqueSolrDocumentsWithHits; + + try { + response = solrServer.query(q, METHOD.POST); + + resultList = response.getResults(); + + // objectId_chunk -> "text" -> List of previews + highlightResponse = response.getHighlighting(); + + // get the unique set of files with hits + uniqueSolrDocumentsWithHits = filterOneHitPerDocument(resultList); + } catch (KeywordSearchModuleException ex) { + logger.log(Level.SEVERE, "Error executing Lucene Solr Query: " + keywordString, ex); //NON-NLS + MessageNotifyUtil.Notify.error(NbBundle.getMessage(Server.class, "Server.query.exception.msg", keywordString), ex.getCause().getMessage()); + return matches; + } // cycle through results in sets of MAX_RESULTS for (int start = 0; !allMatchesFetched; start = start + MAX_RESULTS) { q.setStart(start); + allMatchesFetched = start + MAX_RESULTS >= resultList.getNumFound(); + + SleuthkitCase sleuthkitCase; try { - QueryResponse response = solrServer.query(q, METHOD.POST); - SolrDocumentList resultList = response.getResults(); - - // objectId_chunk -> "text" -> List of previews - Map>> highlightResponse = response.getHighlighting(); - - // get the unique set of files with hits - Set uniqueSolrDocumentsWithHits = filterOneHitPerDocument(resultList); - - allMatchesFetched = start + MAX_RESULTS >= resultList.getNumFound(); - - SleuthkitCase sleuthkitCase; - try { - sleuthkitCase = Case.getCurrentCase().getSleuthkitCase(); - } catch (IllegalStateException ex) { - //no case open, must be just closed - return matches; - } - - for (SolrDocument resultDoc : uniqueSolrDocumentsWithHits) { - KeywordHit contentHit; - try { - contentHit = createKeywordtHit(resultDoc, highlightResponse, sleuthkitCase); - } catch (TskException ex) { - return matches; - } - matches.add(contentHit); - } - - } catch (NoOpenCoreException ex) { - logger.log(Level.WARNING, "Error executing Lucene Solr Query: " + keywordString, ex); //NON-NLS - throw ex; - } catch (KeywordSearchModuleException ex) { - logger.log(Level.WARNING, "Error executing Lucene Solr Query: " + keywordString, ex); //NON-NLS + sleuthkitCase = Case.getCurrentCase().getSleuthkitCase(); + } catch (IllegalStateException ex) { + //no case open, must be just closed + return matches; } + for (SolrDocument resultDoc : uniqueSolrDocumentsWithHits) { + KeywordHit contentHit; + try { + contentHit = createKeywordtHit(resultDoc, highlightResponse, sleuthkitCase); + } catch (TskException ex) { + return matches; + } + matches.add(contentHit); + } } return matches; } @@ -500,9 +503,9 @@ class LuceneQuery implements KeywordSearchQuery { rightID = rightID.substring(0, index); } - Integer leftInt = new Integer(leftID); - Integer rightInt = new Integer(rightID); - return leftInt.compareTo(rightInt); + Long leftLong = new Long(leftID); + Long rightLong = new Long(rightID); + return leftLong.compareTo(rightLong); } } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java index 403b82daf8..50d5e962bd 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java @@ -33,7 +33,7 @@ import java.net.ConnectException; import java.net.ServerSocket; import java.net.SocketException; import java.nio.charset.Charset; -import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; @@ -44,7 +44,6 @@ import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; import javax.swing.AbstractAction; import org.apache.solr.client.solrj.SolrQuery; -import org.apache.solr.client.solrj.SolrServer; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.request.CoreAdminRequest; import org.apache.solr.client.solrj.response.QueryResponse; @@ -60,8 +59,13 @@ import org.sleuthkit.autopsy.coreutils.PlatformUtil; import org.sleuthkit.datamodel.Content; import org.apache.solr.common.SolrInputDocument; import org.apache.solr.client.solrj.impl.XMLResponseParser; +import org.apache.solr.client.solrj.response.CoreAdminResponse; import org.apache.solr.common.SolrDocument; import org.apache.solr.common.SolrException; +import org.sleuthkit.autopsy.casemodule.Case.CaseType; +import org.sleuthkit.autopsy.coreutils.UNCPathUtilities; +import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; /** * Handles for keeping track of a Solr server and its cores @@ -148,7 +152,6 @@ public class Server { public static final long MAX_CONTENT_SIZE = 1L * 1024 * 1024 * 1024; private static final Logger logger = Logger.getLogger(Server.class.getName()); private static final String DEFAULT_CORE_NAME = "coreCase"; //NON-NLS - // TODO: DEFAULT_CORE_NAME needs to be replaced with unique names to support multiple open cases public static final String CORE_EVT = "CORE_EVT"; //NON-NLS public static final char ID_CHUNK_SEP = '_'; private String javaPath = "java"; //NON-NLS @@ -160,33 +163,39 @@ public class Server { static final String PROPERTIES_CURRENT_SERVER_PORT = "IndexingServerPort"; //NON-NLS static final String PROPERTIES_CURRENT_STOP_PORT = "IndexingServerStopPort"; //NON-NLS private static final String KEY = "jjk#09s"; //NON-NLS + static final String DEFAULT_SOLR_SERVER_HOST = "localhost"; //NON-NLS static final int DEFAULT_SOLR_SERVER_PORT = 23232; static final int DEFAULT_SOLR_STOP_PORT = 34343; private int currentSolrServerPort = 0; private int currentSolrStopPort = 0; private static final boolean DEBUG = false;//(Version.getBuildType() == Version.Type.DEVELOPMENT); + private final UNCPathUtilities uncPathUtilities = new UNCPathUtilities(); public enum CORE_EVT_STATES { STOPPED, STARTED }; - private SolrServer solrServer; - private String instanceDir; - private File solrFolder; - private ServerAction serverAction; + + // A reference to the locally running Solr instance. + private final HttpSolrServer localSolrServer; + + // A reference to the Solr server we are currently connected to for the Case. + // This could be a local or remote server. + private HttpSolrServer currentSolrServer; + + private final String instanceDir; + private final File solrFolder; + private final ServerAction serverAction; private InputStreamPrinterThread errorRedirectThread; - private String solrUrl; /** * New instance for the server at the given URL * - * @param url should be something like "http://localhost:23232/solr/" */ Server() { initSettings(); - this.solrUrl = "http://localhost:" + currentSolrServerPort + "/solr"; //NON-NLS - this.solrServer = new HttpSolrServer(solrUrl); + this.localSolrServer = new HttpSolrServer("http://localhost:" + currentSolrServerPort + "/solr"); //NON-NLS serverAction = new ServerAction(); solrFolder = InstalledFileLocator.getDefault().locate("solr", Server.class.getPackage().getName(), false); //NON-NLS instanceDir = solrFolder.getAbsolutePath() + File.separator + "solr"; //NON-NLS @@ -196,6 +205,7 @@ public class Server { } private void initSettings() { + if (ModuleSettings.settingExists(PROPERTIES_FILE, PROPERTIES_CURRENT_SERVER_PORT)) { try { currentSolrServerPort = Integer.decode(ModuleSettings.getConfigSetting(PROPERTIES_FILE, PROPERTIES_CURRENT_SERVER_PORT)); @@ -325,10 +335,10 @@ public class Server { * @return */ List getSolrPIDs() { - List pids = new ArrayList(); + List pids = new ArrayList<>(); //NOTE: these needs to be in sync with process start string in start() - final String pidsQuery = "Args.4.eq=-DSTOP.KEY=" + KEY + ",Args.7.eq=start.jar"; //NON-NLS + final String pidsQuery = "Args.4.eq=-DSTOP.KEY=" + KEY + ",Args.6.eq=start.jar"; //NON-NLS long[] pidsArr = PlatformUtil.getJavaPIDs(pidsQuery); if (pidsArr != null) { @@ -353,45 +363,79 @@ public class Server { } /** - * Tries to start a Solr instance in a separate process. Returns immediately - * (probably before the server is ready) and doesn't check whether it was - * successful. + * Tries to start a local Solr instance in a separate process. Returns + * immediately (probably before the server is ready) and doesn't check + * whether it was successful. */ void start() throws KeywordSearchModuleException, SolrServerNoPortException { + if (isRunning()) { + // If a Solr server is running we stop it. + stop(); + } + + if (!isPortAvailable(currentSolrServerPort)) { + // There is something already listening on our port. Let's see if + // this is from an earlier run that didn't successfully shut down + // and if so kill it. + final List pids = this.getSolrPIDs(); + + // If the culprit listening on the port is not a Solr process + // we refuse to start. + if (pids.isEmpty()) { + throw new SolrServerNoPortException(currentSolrServerPort); + } + + // Ok, we've tried to stop it above but there still appears to be + // a Solr process listening on our port so we forcefully kill it. + killSolr(); + + // If either of the ports are still in use after our attempt to kill + // previously running processes we give up and throw an exception. + if (!isPortAvailable(currentSolrServerPort)) { + throw new SolrServerNoPortException(currentSolrServerPort); + } + if (!isPortAvailable(currentSolrStopPort)) { + throw new SolrServerNoPortException(currentSolrStopPort); + } + } + logger.log(Level.INFO, "Starting Solr server from: " + solrFolder.getAbsolutePath()); //NON-NLS + if (isPortAvailable(currentSolrServerPort)) { logger.log(Level.INFO, "Port [" + currentSolrServerPort + "] available, starting Solr"); //NON-NLS try { final String MAX_SOLR_MEM_MB_PAR = "-Xmx" + Integer.toString(MAX_SOLR_MEM_MB) + "m"; //NON-NLS - String loggingPropertiesOpt = "-Djava.util.logging.config.file="; //NON-NLS - String loggingPropertiesFilePath = instanceDir + File.separator + "conf" + File.separator; //NON-NLS +// String loggingPropertiesOpt = "-Djava.util.logging.config.file="; //NON-NLS +// String loggingPropertiesFilePath = instanceDir + File.separator + "conf" + File.separator; //NON-NLS +// +// if (DEBUG) { +// loggingPropertiesFilePath += "logging-development.properties"; //NON-NLS +// } else { +// loggingPropertiesFilePath += "logging-release.properties"; //NON-NLS +// } +// final String loggingProperties = loggingPropertiesOpt + loggingPropertiesFilePath; + List commandLine = new ArrayList<>(); + commandLine.add(javaPath); + commandLine.add(MAX_SOLR_MEM_MB_PAR); + commandLine.add("-DSTOP.PORT=" + currentSolrStopPort); //NON-NLS + commandLine.add("-Djetty.port=" + currentSolrServerPort); //NON-NLS + commandLine.add("-DSTOP.KEY=" + KEY); //NON-NLS + commandLine.add("-jar"); //NON-NLS + commandLine.add("start.jar"); //NON-NLS - if (DEBUG) { - loggingPropertiesFilePath += "logging-development.properties"; //NON-NLS - } else { - loggingPropertiesFilePath += "logging-release.properties"; //NON-NLS - } + ProcessBuilder solrProcessBuilder = new ProcessBuilder(commandLine); + solrProcessBuilder.directory(solrFolder); - final String loggingProperties = loggingPropertiesOpt + loggingPropertiesFilePath; + // Redirect stdout and stderr to files to prevent blocking. + Path solrStdoutPath = Paths.get(Places.getUserDirectory().getAbsolutePath(), "var", "log", "solr.log.stdout"); + solrProcessBuilder.redirectOutput(solrStdoutPath.toFile()); - final String[] SOLR_START_CMD = { - javaPath, - MAX_SOLR_MEM_MB_PAR, - "-DSTOP.PORT=" + currentSolrStopPort, //NON-NLS - "-Djetty.port=" + currentSolrServerPort, //NON-NLS - "-DSTOP.KEY=" + KEY, //NON-NLS - loggingProperties, - "-jar", //NON-NLS - "start.jar"}; //NON-NLS + Path solrStderrPath = Paths.get(Places.getUserDirectory().getAbsolutePath(), "var", "log", "solr.log.stderr"); + solrProcessBuilder.redirectError(solrStderrPath.toFile()); - StringBuilder cmdSb = new StringBuilder(); - for (int i = 0; i < SOLR_START_CMD.length; ++i) { - cmdSb.append(SOLR_START_CMD[i]).append(" "); - } - - logger.log(Level.INFO, "Starting Solr using: " + cmdSb.toString()); //NON-NLS - curSolrProcess = Runtime.getRuntime().exec(SOLR_START_CMD, null, solrFolder); + logger.log(Level.INFO, "Starting Solr using: " + solrProcessBuilder.command()); //NON-NLS + curSolrProcess = solrProcessBuilder.start(); logger.log(Level.INFO, "Finished starting Solr"); //NON-NLS try { @@ -401,10 +445,6 @@ public class Server { } catch (InterruptedException ex) { logger.log(Level.WARNING, "Timer interrupted"); //NON-NLS } - // Handle output to prevent process from blocking - - errorRedirectThread = new InputStreamPrinterThread(curSolrProcess.getErrorStream(), "stderr"); //NON-NLS - errorRedirectThread.start(); final List pids = this.getSolrPIDs(); logger.log(Level.INFO, "New Solr process PID: " + pids); //NON-NLS @@ -417,9 +457,6 @@ public class Server { throw new KeywordSearchModuleException( NbBundle.getMessage(this.getClass(), "Server.start.exception.cantStartSolr.msg2"), ex); } - } else { - logger.log(Level.SEVERE, "Could not start Solr server process, port [" + currentSolrServerPort + "] not available!"); //NON-NLS - throw new SolrServerNoPortException(currentSolrServerPort); } } @@ -475,13 +512,22 @@ public class Server { } /** - * Tries to stop a Solr instance. + * Tries to stop the local Solr instance. * * Waits for the stop command to finish before returning. */ synchronized void stop() { + + try { + // Close any open core before stopping server + closeCore(); + } catch (KeywordSearchModuleException e) { + logger.log(Level.WARNING, "Failed to close core: ", e); //NON-NLS + } + try { logger.log(Level.INFO, "Stopping Solr server from: " + solrFolder.getAbsolutePath()); //NON-NLS + //try graceful shutdown final String[] SOLR_STOP_CMD = { javaPath, @@ -501,8 +547,7 @@ public class Server { curSolrProcess = null; } - } catch (InterruptedException ex) { - } catch (IOException ex) { + } catch (InterruptedException | IOException ex) { } finally { //stop Solr stream -> log redirect threads try { @@ -520,7 +565,7 @@ public class Server { } /** - * Tests if there's a Solr server running by sending it a core-status + * Tests if there's a local Solr server running by sending it a core-status * request. * * @return false if the request failed with a connection error, otherwise @@ -528,12 +573,19 @@ public class Server { */ synchronized boolean isRunning() throws KeywordSearchModuleException { try { + + if (isPortAvailable(currentSolrServerPort)) { + return false; + } + + if (curSolrProcess != null && !curSolrProcess.isAlive()) { + return false; + } + // making a status request here instead of just doing solrServer.ping(), because // that doesn't work when there are no cores - - //TODO check if port avail and return false if it is //TODO handle timeout in cases when some other type of server on that port - CoreAdminRequest.getStatus(null, solrServer); + CoreAdminRequest.getStatus(null, localSolrServer); logger.log(Level.INFO, "Solr server is running"); //NON-NLS } catch (SolrServerException ex) { @@ -574,43 +626,20 @@ public class Server { Case currentCase = Case.getCurrentCase(); - validateIndexLocation(currentCase); - - currentCore = openCore(currentCase); - serverAction.putValue(CORE_EVT, CORE_EVT_STATES.STARTED); - } - - /** - * Checks if index dir exists, and moves it to new location if needed (for - * backwards compatibility with older cases) - */ - private void validateIndexLocation(Case theCase) { - logger.log(Level.INFO, "Validating keyword search index location"); //NON-NLS - String properIndexPath = getIndexDirPath(theCase); - - String legacyIndexPath = theCase.getCaseDirectory() - + File.separator + "keywordsearch" + File.separator + "data"; //NON-NLS - - File properIndexDir = new File(properIndexPath); - File legacyIndexDir = new File(legacyIndexPath); - if (!properIndexDir.exists() - && legacyIndexDir.exists() && legacyIndexDir.isDirectory()) { - logger.log(Level.INFO, "Moving keyword search index location from: " //NON-NLS - + legacyIndexPath + " to: " + properIndexPath); //NON-NLS - try { - Files.move(Paths.get(legacyIndexDir.getParent()), Paths.get(properIndexDir.getParent())); - } catch (IOException | SecurityException ex) { - logger.log(Level.WARNING, "Error moving keyword search index folder from: " //NON-NLS - + legacyIndexPath + " to: " + properIndexPath //NON-NLS - + " will recreate a new index.", ex); //NON-NLS - } + try { + currentCore = openCore(currentCase); + } catch (KeywordSearchModuleException ex) { + MessageNotifyUtil.Notify.error(NbBundle.getMessage(Server.class, "Server.openCore.exception.cantOpenForCase.msg", currentCase.getName()), ex.getCause().getMessage()); + throw ex; } + serverAction.putValue(CORE_EVT, CORE_EVT_STATES.STARTED); } synchronized void closeCore() throws KeywordSearchModuleException { if (currentCore == null) { return; } + currentCore.close(); currentCore = null; serverAction.putValue(CORE_EVT, CORE_EVT_STATES.STOPPED); @@ -628,9 +657,16 @@ public class Server { * @return absolute path to index dir */ String getIndexDirPath(Case theCase) { - String indexDir = theCase.getModulesOutputDirAbsPath() - + File.separator + "keywordsearch" + File.separator + "data"; //NON-NLS - return indexDir; + String indexDir = theCase.getModuleDirectory() + File.separator + "keywordsearch" + File.separator + "data"; //NON-NLS + String result = uncPathUtilities.mappedDriveToUNC(indexDir); + if (result == null) { + uncPathUtilities.rescanDrives(); + result = uncPathUtilities.mappedDriveToUNC(indexDir); + } + if (result == null) { + return indexDir; + } + return result; } /** @@ -644,8 +680,24 @@ public class Server { * @return */ private synchronized Core openCore(Case theCase) throws KeywordSearchModuleException { + try { + if (theCase.getCaseType() == CaseType.SINGLE_USER_CASE) { + currentSolrServer = this.localSolrServer; + } else { + String host = UserPreferences.getIndexingServerHost(); + String port = UserPreferences.getIndexingServerPort(); + + currentSolrServer = new HttpSolrServer("http://" + host + ":" + port + "/solr"); //NON-NLS + } + connectToSolrServer(currentSolrServer); + } catch (SolrServerException | IOException ex) { + MessageNotifyUtil.Notify.error(NbBundle.getMessage(Server.class, "Server.connect.exception.msg"), ex.getCause().getMessage()); + throw new KeywordSearchModuleException(NbBundle.getMessage(Server.class, "Server.connect.exception.msg")); + } + String dataDir = getIndexDirPath(theCase); - return this.openCore(DEFAULT_CORE_NAME, new File(dataDir)); + String coreName = theCase.getTextIndexName(); + return this.openCore(coreName.isEmpty() ? DEFAULT_CORE_NAME : coreName, new File(dataDir), theCase.getCaseType()); } /** @@ -675,7 +727,7 @@ public class Server { * @return int representing number of indexed files * * @throws KeywordSearchModuleException - * @throws NoOpenCoreExceptionn + * @throws NoOpenCoreException */ public int queryNumIndexedFiles() throws KeywordSearchModuleException, NoOpenCoreException { if (currentCore == null) { @@ -943,7 +995,8 @@ public class Server { * * @return new core */ - private Core openCore(String coreName, File dataDir) throws KeywordSearchModuleException { + private Core openCore(String coreName, File dataDir, CaseType caseType) throws KeywordSearchModuleException { + try { if (!dataDir.exists()) { dataDir.mkdirs(); @@ -956,18 +1009,22 @@ public class Server { NbBundle.getMessage(this.getClass(), "Server.openCore.exception.msg")); } - CoreAdminRequest.Create createCore = new CoreAdminRequest.Create(); - createCore.setDataDir(dataDir.getAbsolutePath()); - createCore.setInstanceDir(instanceDir); - createCore.setCoreName(coreName); + if (!isCoreLoaded(coreName)) { + CoreAdminRequest.Create createCore = new CoreAdminRequest.Create(); + createCore.setDataDir(dataDir.getAbsolutePath()); + createCore.setCoreName(coreName); + createCore.setConfigSet("AutopsyConfig"); //NON-NLS + createCore.setIsLoadOnStartup(false); + createCore.setIsTransient(true); - this.solrServer.request(createCore); + currentSolrServer.request(createCore); + } - final Core newCore = new Core(coreName); + final Core newCore = new Core(coreName, caseType); return newCore; - } catch (SolrServerException ex) { + } catch (SolrServerException | SolrException ex) { throw new KeywordSearchModuleException( NbBundle.getMessage(this.getClass(), "Server.openCore.exception.cantOpen.msg"), ex); } catch (IOException ex) { @@ -976,18 +1033,49 @@ public class Server { } } + /** + * Attempts to connect to the given Solr server. + * + * @param solrServer + * + * @throws SolrServerException + * @throws IOException + */ + void connectToSolrServer(HttpSolrServer solrServer) throws SolrServerException, IOException { + CoreAdminRequest.getStatus(null, solrServer); + } + + /** + * Determines whether the Solr core with the given name already exists. + * + * @param coreName + * + * @return true if core exists, otherwise false. + * + * @throws SolrServerException + * @throws IOException + */ + private boolean isCoreLoaded(String coreName) throws SolrServerException, IOException { + CoreAdminResponse response = CoreAdminRequest.getStatus(coreName, currentSolrServer); + return response.getCoreStatus(coreName).get("instanceDir") != null; //NON-NLS + } + class Core { // handle to the core in Solr - private String name; + private final String name; + + private final CaseType caseType; + // the server to access a core needs to be built from a URL with the // core in it, and is only good for core-specific operations - private HttpSolrServer solrCore; + private final HttpSolrServer solrCore; - private Core(String name) { + private Core(String name, CaseType caseType) { this.name = name; + this.caseType = caseType; - this.solrCore = new HttpSolrServer(solrUrl + "/" + name); + this.solrCore = new HttpSolrServer(currentSolrServer.getBaseURL() + "/" + name); //TODO test these settings //solrCore.setSoTimeout(1000 * 60); // socket read timeout, make large enough so can index larger files @@ -1091,8 +1179,13 @@ public class Server { } synchronized void close() throws KeywordSearchModuleException { + // We only unload cores for "single-user" cases. + if (this.caseType == CaseType.MULTI_USER_CASE) { + return; + } + try { - CoreAdminRequest.unloadCore(this.name, solrServer); + CoreAdminRequest.unloadCore(this.name, currentSolrServer); } catch (SolrServerException ex) { throw new KeywordSearchModuleException( NbBundle.getMessage(this.getClass(), "Server.close.exception.msg"), ex); diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/SolrSearchService.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/SolrSearchService.java index 3a2599e128..dbc6598e2c 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/SolrSearchService.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/SolrSearchService.java @@ -20,6 +20,8 @@ package org.sleuthkit.autopsy.keywordsearch; import java.io.IOException; import java.util.HashMap; +import org.apache.solr.client.solrj.SolrServerException; +import org.apache.solr.client.solrj.impl.HttpSolrServer; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.TskCoreException; @@ -31,6 +33,10 @@ import org.sleuthkit.autopsy.datamodel.ContentUtils; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.SleuthkitCase; +import org.openide.util.NbBundle; +import java.net.InetAddress; +import java.util.MissingResourceException; +import org.sleuthkit.autopsy.keywordsearchservice.KeywordSearchServiceException; /** * An implementation of the KeywordSearchService interface that uses Solr for @@ -39,6 +45,10 @@ import org.sleuthkit.datamodel.SleuthkitCase; @ServiceProvider(service = KeywordSearchService.class) public class SolrSearchService implements KeywordSearchService { + private static final String BAD_IP_ADDRESS_FORMAT = "ioexception occured when talking to server"; + private static final String SERVER_REFUSED_CONNECTION = "server refused connection"; + private static final int IS_REACHABLE_TIMEOUT_MS = 1000; + @Override public void indexArtifact(BlackboardArtifact artifact) throws TskCoreException { if (artifact == null) { @@ -53,8 +63,11 @@ public class SolrSearchService implements KeywordSearchService { return; } - Case currentCase = Case.getCurrentCase(); - if (currentCase == null) { + Case currentCase; + try { + currentCase = Case.getCurrentCase(); + } catch (IllegalStateException ignore) { + // thorown by Case.getCurrentCase() if currentCase is null return; } @@ -103,7 +116,8 @@ public class SolrSearchService implements KeywordSearchService { || attribute.getAttributeTypeID() == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_SENT.getTypeID() || attribute.getAttributeTypeID() == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_START.getTypeID() || attribute.getAttributeTypeID() == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_END.getTypeID()) { - artifactContents.append(ContentUtils.getStringTime(attribute.getValueLong(), abstractFile)); + + artifactContents.append(ContentUtils.getStringTime(attribute.getValueLong(), dataSource)); } else { artifactContents.append(attribute.getDisplayString()); } @@ -149,6 +163,63 @@ public class SolrSearchService implements KeywordSearchService { } } + /** + * Checks if we can communicate with Solr using the passed-in host and port. + * Closes the connection upon exit. Throws if it cannot communicate with + * Solr. + * + * When issues occur, it attempts to diagnose them by looking at the + * exception messages, returning the appropriate user-facing text for the + * exception received. This method expects the Exceptions messages to be in + * English and compares against English text. + * + * @param host the remote hostname or IP address of the Solr server + * @param port the remote port for Solr + * + * @throws org.sleuthkit.autopsy.keywordsearch.KeywordSearchServiceException + * + */ + @Override + public void tryConnect(String host, int port) throws KeywordSearchServiceException { + HttpSolrServer solrServer = null; + if (host == null || host.isEmpty()) { + throw new KeywordSearchServiceException(NbBundle.getMessage(SolrSearchService.class, "SolrConnectionCheck.MissingHostname")); //NON-NLS + } + try { + solrServer = new HttpSolrServer("http://" + host + ":" + Integer.toString(port) + "/solr"); //NON-NLS; + KeywordSearch.getServer().connectToSolrServer(solrServer); + } catch (SolrServerException ex) { + throw new KeywordSearchServiceException(NbBundle.getMessage(SolrSearchService.class, "SolrConnectionCheck.HostnameOrPort")); //NON-NLS + } catch (IOException ex) { + String result = NbBundle.getMessage(SolrSearchService.class, "SolrConnectionCheck.HostnameOrPort"); //NON-NLS + String message = ex.getCause().getMessage().toLowerCase(); + if (message.startsWith(SERVER_REFUSED_CONNECTION)) { + try { + if (InetAddress.getByName(host).isReachable(IS_REACHABLE_TIMEOUT_MS)) { + // if we can reach the host, then it's probably port problem + result = NbBundle.getMessage(SolrSearchService.class, "SolrConnectionCheck.Port"); //NON-NLS + } else { + result = NbBundle.getMessage(SolrSearchService.class, "SolrConnectionCheck.HostnameOrPort"); //NON-NLS + } + } catch (IOException | MissingResourceException any) { + // it may be anything + result = NbBundle.getMessage(SolrSearchService.class, "SolrConnectionCheck.HostnameOrPort"); //NON-NLS + } + } else if (message.startsWith(BAD_IP_ADDRESS_FORMAT)) { + result = NbBundle.getMessage(SolrSearchService.class, "SolrConnectionCheck.Hostname"); //NON-NLS + } + throw new KeywordSearchServiceException(result); + } catch (NumberFormatException ex) { + throw new KeywordSearchServiceException(NbBundle.getMessage(SolrSearchService.class, "SolrConnectionCheck.Port")); //NON-NLS + } catch (IllegalArgumentException ex) { + throw new KeywordSearchServiceException(ex.getMessage()); + } finally { + if (null != solrServer) { + solrServer.shutdown(); + } + } + } + @Override public void close() throws IOException { } diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Chrome.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Chrome.java index 3f24c19511..5d7ecd2f5b 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Chrome.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Chrome.java @@ -56,11 +56,11 @@ import org.sleuthkit.datamodel.TskData; class Chrome extends Extract { private static final String historyQuery = "SELECT urls.url, urls.title, urls.visit_count, urls.typed_count, " //NON-NLS - + "last_visit_time, urls.hidden, visits.visit_time, (SELECT urls.url FROM urls WHERE urls.id=visits.url) as from_visit, visits.transition FROM urls, visits WHERE urls.id = visits.url"; //NON-NLS - private static final String cookieQuery = "select name, value, host_key, expires_utc,last_access_utc, creation_utc from cookies"; //NON-NLS - private static final String downloadQuery = "select full_path, url, start_time, received_bytes from downloads"; //NON-NLS - private static final String downloadQueryVersion30 = "SELECT current_path as full_path, url, start_time, received_bytes FROM downloads, downloads_url_chains WHERE downloads.id=downloads_url_chains.id"; //NON-NLS - private static final String loginQuery = "select origin_url, username_value, signon_realm from logins"; //NON-NLS + + "last_visit_time, urls.hidden, visits.visit_time, (SELECT urls.url FROM urls WHERE urls.id=visits.url) AS from_visit, visits.transition FROM urls, visits WHERE urls.id = visits.url"; //NON-NLS + private static final String cookieQuery = "SELECT name, value, host_key, expires_utc,last_access_utc, creation_utc FROM cookies"; //NON-NLS + private static final String downloadQuery = "SELECT full_path, url, start_time, received_bytes FROM downloads"; //NON-NLS + private static final String downloadQueryVersion30 = "SELECT current_path AS full_path, url, start_time, received_bytes FROM downloads, downloads_url_chains WHERE downloads.id=downloads_url_chains.id"; //NON-NLS + private static final String loginQuery = "SELECT origin_url, username_value, signon_realm from logins"; //NON-NLS private final Logger logger = Logger.getLogger(this.getClass().getName()); private Content dataSource; private IngestJobContext context; diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Firefox.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Firefox.java index 77d852da4e..c94be3adba 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Firefox.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Firefox.java @@ -53,12 +53,12 @@ import org.sleuthkit.datamodel.TskCoreException; class Firefox extends Extract { private static final Logger logger = Logger.getLogger(Firefox.class.getName()); - private static final String historyQuery = "SELECT moz_historyvisits.id,url,title,visit_count,(visit_date/1000000) as visit_date,from_visit,(SELECT url FROM moz_places WHERE id=moz_historyvisits.from_visit) as ref FROM moz_places, moz_historyvisits WHERE moz_places.id = moz_historyvisits.place_id AND hidden = 0"; //NON-NLS - private static final String cookieQuery = "SELECT name,value,host,expiry,(lastAccessed/1000000) as lastAccessed,(creationTime/1000000) as creationTime FROM moz_cookies"; //NON-NLS - private static final String cookieQueryV3 = "SELECT name,value,host,expiry,(lastAccessed/1000000) as lastAccessed FROM moz_cookies"; //NON-NLS - private static final String bookmarkQuery = "SELECT fk, moz_bookmarks.title, url, (moz_bookmarks.dateAdded/1000000) as dateAdded FROM moz_bookmarks INNER JOIN moz_places ON moz_bookmarks.fk=moz_places.id"; //NON-NLS - private static final String downloadQuery = "SELECT target, source,(startTime/1000000) as startTime, maxBytes FROM moz_downloads"; //NON-NLS - private static final String downloadQueryVersion24 = "SELECT url, content as target, (lastModified/1000000) as lastModified FROM moz_places, moz_annos WHERE moz_places.id = moz_annos.place_id AND moz_annos.anno_attribute_id = 3"; //NON-NLS + private static final String historyQuery = "SELECT moz_historyvisits.id,url,title,visit_count,(visit_date/1000000) AS visit_date,from_visit,(SELECT url FROM moz_places WHERE id=moz_historyvisits.from_visit) as ref FROM moz_places, moz_historyvisits WHERE moz_places.id = moz_historyvisits.place_id AND hidden = 0"; //NON-NLS + private static final String cookieQuery = "SELECT name,value,host,expiry,(lastAccessed/1000000) AS lastAccessed,(creationTime/1000000) AS creationTime FROM moz_cookies"; //NON-NLS + private static final String cookieQueryV3 = "SELECT name,value,host,expiry,(lastAccessed/1000000) AS lastAccessed FROM moz_cookies"; //NON-NLS + private static final String bookmarkQuery = "SELECT fk, moz_bookmarks.title, url, (moz_bookmarks.dateAdded/1000000) AS dateAdded FROM moz_bookmarks INNER JOIN moz_places ON moz_bookmarks.fk=moz_places.id"; //NON-NLS + private static final String downloadQuery = "SELECT target, source,(startTime/1000000) AS startTime, maxBytes FROM moz_downloads"; //NON-NLS + private static final String downloadQueryVersion24 = "SELECT url, content AS target, (lastModified/1000000) AS lastModified FROM moz_places, moz_annos WHERE moz_places.id = moz_annos.place_id AND moz_annos.anno_attribute_id = 3"; //NON-NLS private final IngestServices services = IngestServices.getInstance(); private Content dataSource; private IngestJobContext context; diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/RAImageIngestModule.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/RAImageIngestModule.java index 6432627881..db9af9087c 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/RAImageIngestModule.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/RAImageIngestModule.java @@ -209,7 +209,7 @@ public final class RAImageIngestModule implements DataSourceIngestModule { * @return Path to directory */ protected static String getRAOutputPath(Case a_case, String mod) { - String tmpDir = a_case.getModulesOutputDirAbsPath() + File.separator + "RecentActivity" + File.separator + mod; //NON-NLS + String tmpDir = a_case.getModuleDirectory() + File.separator + "RecentActivity" + File.separator + mod; //NON-NLS File dir = new File(tmpDir); if (dir.exists() == false) { dir.mkdirs(); diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/SearchEngineURLQueryAnalyzer.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/SearchEngineURLQueryAnalyzer.java index ff6ba38379..1d144c317a 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/SearchEngineURLQueryAnalyzer.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/SearchEngineURLQueryAnalyzer.java @@ -278,13 +278,13 @@ class SearchEngineURLQueryAnalyzer extends Extract { int totalQueries = 0; try { //from blackboard_artifacts - Collection listArtifacts = currentCase.getSleuthkitCase().getMatchingArtifacts("WHERE (`artifact_type_id` = '" + ARTIFACT_TYPE.TSK_WEB_BOOKMARK.getTypeID() //NON-NLS - + "' OR `artifact_type_id` = '" + ARTIFACT_TYPE.TSK_WEB_HISTORY.getTypeID() + "') "); //List of every 'web_history' and 'bookmark' artifact NON-NLS + Collection listArtifacts = currentCase.getSleuthkitCase().getMatchingArtifacts("WHERE (artifact_type_id = '" + ARTIFACT_TYPE.TSK_WEB_BOOKMARK.getTypeID() //NON-NLS + + "' OR artifact_type_id = '" + ARTIFACT_TYPE.TSK_WEB_HISTORY.getTypeID() + "') "); //List of every 'web_history' and 'bookmark' artifact NON-NLS logger.log(Level.INFO, "Processing {0} blackboard artifacts.", listArtifacts.size()); //NON-NLS for (BlackboardArtifact artifact : listArtifacts) { if (context.dataSourceIngestIsCancelled()) { - break; //User cancled the process. + break; //User cancelled the process. } //initializing default attributes @@ -307,7 +307,7 @@ class SearchEngineURLQueryAnalyzer extends Extract { SearchEngineURLQueryAnalyzer.SearchEngine se = null; //from blackboard_attributes - Collection listAttributes = currentCase.getSleuthkitCase().getMatchingAttributes("Where `artifact_id` = " + artifact.getArtifactID()); //NON-NLS + Collection listAttributes = currentCase.getSleuthkitCase().getMatchingAttributes("WHERE artifact_id = " + artifact.getArtifactID()); //NON-NLS for (BlackboardAttribute attribute : listAttributes) { if (attribute.getAttributeTypeID() == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL.getTypeID()) { diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Util.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Util.java index 081451f337..a69ee8edf7 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Util.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Util.java @@ -190,8 +190,6 @@ class Util { int index = parent_path.lastIndexOf('/'); String name = parent_path.substring(++index); parent_path = parent_path.substring(0, index); - //String query = "select * from tsk_files where parent_path like \"" + parent_path + "\" AND name like \"" + name + "\""; - FileManager fileManager = Case.getCurrentCase().getServices().getFileManager(); List files = null; try { diff --git a/ScalpelCarver/src/org/sleuthkit/autopsy/scalpel/ScalpelCarverIngestModule.java b/ScalpelCarver/src/org/sleuthkit/autopsy/scalpel/ScalpelCarverIngestModule.java index 7101c71dc4..d6318a02de 100644 --- a/ScalpelCarver/src/org/sleuthkit/autopsy/scalpel/ScalpelCarverIngestModule.java +++ b/ScalpelCarver/src/org/sleuthkit/autopsy/scalpel/ScalpelCarverIngestModule.java @@ -83,7 +83,7 @@ class ScalpelCarverIngestModule implements FileIngestModule { } // make sure module output directory exists; create it if it doesn't - moduleOutputDirPath = Case.getCurrentCase().getModulesOutputDirAbsPath() + moduleOutputDirPath = Case.getCurrentCase().getModuleDirectory() + File.separator + MODULE_OUTPUT_DIR_NAME; File moduleOutputDir = new File(moduleOutputDirPath); if (!moduleOutputDir.exists()) { diff --git a/Testing/test/qa-functional/src/org/sleuthkit/autopsy/testing/RegressionTest.java b/Testing/test/qa-functional/src/org/sleuthkit/autopsy/testing/RegressionTest.java index d8320f325c..bcc36a08a5 100755 --- a/Testing/test/qa-functional/src/org/sleuthkit/autopsy/testing/RegressionTest.java +++ b/Testing/test/qa-functional/src/org/sleuthkit/autopsy/testing/RegressionTest.java @@ -174,9 +174,9 @@ public class RegressionTest extends TestCase { public void testNewCaseWizard() { logger.info("New Case Wizard"); WizardOperator wo = new WizardOperator("New Case Information"); - JTextFieldOperator jtfo1 = new JTextFieldOperator(wo, 1); + JTextFieldOperator jtfo1 = new JTextFieldOperator(wo, 2); jtfo1.typeText("AutopsyTestCase"); // Name the case "AutopsyTestCase" - JTextFieldOperator jtfo0 = new JTextFieldOperator(wo, 0); + JTextFieldOperator jtfo0 = new JTextFieldOperator(wo, 1); jtfo0.typeText(getEscapedPath(System.getProperty("out_path"))); wo.btNext().clickMouse(); JTextFieldOperator jtfo2 = new JTextFieldOperator(wo, 0); diff --git a/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties b/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties index a9cb0e1612..a41dd17420 100644 --- a/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties +++ b/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties @@ -1,5 +1,5 @@ #Updated by build script -#Thu, 23 Jul 2015 12:41:04 -0400 +#Tue, 13 Oct 2015 17:00:33 -0400 LBL_splash_window_title=Starting Autopsy SPLASH_HEIGHT=314 SPLASH_WIDTH=538 diff --git a/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties b/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties index 50d6e54453..5dd3fa12e1 100644 --- a/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties +++ b/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties @@ -1,5 +1,4 @@ #Updated by build script -#Thu, 23 Jul 2015 12:41:04 -0400 - +#Tue, 13 Oct 2015 17:00:33 -0400 CTL_MainWindow_Title=Autopsy 3.1.3 CTL_MainWindow_Title_No_Project=Autopsy 3.1.3 diff --git a/build-windows.xml b/build-windows.xml index 768173d662..e4335ad9b7 100644 --- a/build-windows.xml +++ b/build-windows.xml @@ -8,7 +8,7 @@ - + @@ -27,10 +27,12 @@ - - + + + + @@ -84,7 +94,6 @@ - diff --git a/build.xml b/build.xml index 485a19dcf4..24e28764ec 100644 --- a/build.xml +++ b/build.xml @@ -2,14 +2,14 @@ - + Builds the module suite Autopsy3. - + @@ -47,14 +47,11 @@ TSK_HOME: ${env.TSK_HOME} - - - - + - + @@ -102,6 +99,10 @@ + + + + @@ -162,7 +163,7 @@ - + @@ -188,7 +189,6 @@ - @@ -196,6 +196,7 @@ + @@ -242,30 +243,4 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/doxygen-user/images/JRE_bitness.PNG b/docs/doxygen-user/images/JRE_bitness.PNG new file mode 100755 index 0000000000..d558822392 Binary files /dev/null and b/docs/doxygen-user/images/JRE_bitness.PNG differ diff --git a/docs/doxygen-user/images/StartActiveMQService.PNG b/docs/doxygen-user/images/StartActiveMQService.PNG new file mode 100755 index 0000000000..2aa68fc0f7 Binary files /dev/null and b/docs/doxygen-user/images/StartActiveMQService.PNG differ diff --git a/docs/doxygen-user/images/StopActiveMQService.PNG b/docs/doxygen-user/images/StopActiveMQService.PNG new file mode 100755 index 0000000000..31b5a0da0c Binary files /dev/null and b/docs/doxygen-user/images/StopActiveMQService.PNG differ diff --git a/docs/doxygen-user/images/activemq.PNG b/docs/doxygen-user/images/activemq.PNG new file mode 100644 index 0000000000..709d998eab Binary files /dev/null and b/docs/doxygen-user/images/activemq.PNG differ diff --git a/docs/doxygen-user/images/apachebadmessage.PNG b/docs/doxygen-user/images/apachebadmessage.PNG new file mode 100644 index 0000000000..b2d7566df4 Binary files /dev/null and b/docs/doxygen-user/images/apachebadmessage.PNG differ diff --git a/docs/doxygen-user/images/case-newcase.png b/docs/doxygen-user/images/case-newcase.png index 5d580db797..40209b9c84 100755 Binary files a/docs/doxygen-user/images/case-newcase.png and b/docs/doxygen-user/images/case-newcase.png differ diff --git a/docs/doxygen-user/images/dataDir.PNG b/docs/doxygen-user/images/dataDir.PNG new file mode 100755 index 0000000000..b4b6eb0693 Binary files /dev/null and b/docs/doxygen-user/images/dataDir.PNG differ diff --git a/docs/doxygen-user/images/groups.properties.after.PNG b/docs/doxygen-user/images/groups.properties.after.PNG new file mode 100755 index 0000000000..c6f8e60344 Binary files /dev/null and b/docs/doxygen-user/images/groups.properties.after.PNG differ diff --git a/docs/doxygen-user/images/groups.properties.before.PNG b/docs/doxygen-user/images/groups.properties.before.PNG new file mode 100755 index 0000000000..be233d6ff7 Binary files /dev/null and b/docs/doxygen-user/images/groups.properties.before.PNG differ diff --git a/docs/doxygen-user/images/insertTextHere.PNG b/docs/doxygen-user/images/insertTextHere.PNG new file mode 100755 index 0000000000..c057e1640e Binary files /dev/null and b/docs/doxygen-user/images/insertTextHere.PNG differ diff --git a/docs/doxygen-user/images/insertedText.PNG b/docs/doxygen-user/images/insertedText.PNG new file mode 100755 index 0000000000..3d7fb34403 Binary files /dev/null and b/docs/doxygen-user/images/insertedText.PNG differ diff --git a/docs/doxygen-user/images/javaproperties.PNG b/docs/doxygen-user/images/javaproperties.PNG new file mode 100644 index 0000000000..317092fa18 Binary files /dev/null and b/docs/doxygen-user/images/javaproperties.PNG differ diff --git a/docs/doxygen-user/images/log4j.PNG b/docs/doxygen-user/images/log4j.PNG new file mode 100755 index 0000000000..15a4ba1113 Binary files /dev/null and b/docs/doxygen-user/images/log4j.PNG differ diff --git a/docs/doxygen-user/images/maxConnections.PNG b/docs/doxygen-user/images/maxConnections.PNG new file mode 100755 index 0000000000..13734ce420 Binary files /dev/null and b/docs/doxygen-user/images/maxConnections.PNG differ diff --git a/docs/doxygen-user/images/maxinactivityduration.PNG b/docs/doxygen-user/images/maxinactivityduration.PNG new file mode 100644 index 0000000000..14401d7361 Binary files /dev/null and b/docs/doxygen-user/images/maxinactivityduration.PNG differ diff --git a/docs/doxygen-user/images/messagebubbles.PNG b/docs/doxygen-user/images/messagebubbles.PNG new file mode 100644 index 0000000000..6557b6326a Binary files /dev/null and b/docs/doxygen-user/images/messagebubbles.PNG differ diff --git a/docs/doxygen-user/images/messagebubblesbigger.PNG b/docs/doxygen-user/images/messagebubblesbigger.PNG new file mode 100644 index 0000000000..a73fd81c86 Binary files /dev/null and b/docs/doxygen-user/images/messagebubblesbigger.PNG differ diff --git a/docs/doxygen-user/images/multiuser_settings.PNG b/docs/doxygen-user/images/multiuser_settings.PNG new file mode 100755 index 0000000000..cb6815c5de Binary files /dev/null and b/docs/doxygen-user/images/multiuser_settings.PNG differ diff --git a/docs/doxygen-user/images/newLoginRole.PNG b/docs/doxygen-user/images/newLoginRole.PNG new file mode 100755 index 0000000000..2ceaf5305f Binary files /dev/null and b/docs/doxygen-user/images/newLoginRole.PNG differ diff --git a/docs/doxygen-user/images/newPassword.PNG b/docs/doxygen-user/images/newPassword.PNG new file mode 100755 index 0000000000..aeaa67fb31 Binary files /dev/null and b/docs/doxygen-user/images/newPassword.PNG differ diff --git a/docs/doxygen-user/images/newRights.PNG b/docs/doxygen-user/images/newRights.PNG new file mode 100755 index 0000000000..548519c757 Binary files /dev/null and b/docs/doxygen-user/images/newRights.PNG differ diff --git a/docs/doxygen-user/images/newUserAndPassword.PNG b/docs/doxygen-user/images/newUserAndPassword.PNG new file mode 100755 index 0000000000..39f90ab846 Binary files /dev/null and b/docs/doxygen-user/images/newUserAndPassword.PNG differ diff --git a/docs/doxygen-user/images/othernodeingesting.PNG b/docs/doxygen-user/images/othernodeingesting.PNG new file mode 100644 index 0000000000..15d73c9e75 Binary files /dev/null and b/docs/doxygen-user/images/othernodeingesting.PNG differ diff --git a/docs/doxygen-user/images/pgAdmin.PNG b/docs/doxygen-user/images/pgAdmin.PNG new file mode 100755 index 0000000000..cca4988335 Binary files /dev/null and b/docs/doxygen-user/images/pgAdmin.PNG differ diff --git a/docs/doxygen-user/images/postgresqlinstall1.PNG b/docs/doxygen-user/images/postgresqlinstall1.PNG new file mode 100644 index 0000000000..4223962bd4 Binary files /dev/null and b/docs/doxygen-user/images/postgresqlinstall1.PNG differ diff --git a/docs/doxygen-user/images/postgresqlinstall2.PNG b/docs/doxygen-user/images/postgresqlinstall2.PNG new file mode 100755 index 0000000000..7a417966ca Binary files /dev/null and b/docs/doxygen-user/images/postgresqlinstall2.PNG differ diff --git a/docs/doxygen-user/images/postgresqlinstall3.PNG b/docs/doxygen-user/images/postgresqlinstall3.PNG new file mode 100644 index 0000000000..37aaf8b7da Binary files /dev/null and b/docs/doxygen-user/images/postgresqlinstall3.PNG differ diff --git a/docs/doxygen-user/images/postgresqlinstall4.PNG b/docs/doxygen-user/images/postgresqlinstall4.PNG new file mode 100644 index 0000000000..76a26ffd92 Binary files /dev/null and b/docs/doxygen-user/images/postgresqlinstall4.PNG differ diff --git a/docs/doxygen-user/images/postgresqlinstall5.PNG b/docs/doxygen-user/images/postgresqlinstall5.PNG new file mode 100644 index 0000000000..d9637ad148 Binary files /dev/null and b/docs/doxygen-user/images/postgresqlinstall5.PNG differ diff --git a/docs/doxygen-user/images/postgresqlinstall6.PNG b/docs/doxygen-user/images/postgresqlinstall6.PNG new file mode 100644 index 0000000000..a2bafe672f Binary files /dev/null and b/docs/doxygen-user/images/postgresqlinstall6.PNG differ diff --git a/docs/doxygen-user/images/postgresqlinstall7.PNG b/docs/doxygen-user/images/postgresqlinstall7.PNG new file mode 100644 index 0000000000..4df64a135f Binary files /dev/null and b/docs/doxygen-user/images/postgresqlinstall7.PNG differ diff --git a/docs/doxygen-user/images/solrinstall1.PNG b/docs/doxygen-user/images/solrinstall1.PNG new file mode 100644 index 0000000000..4ecc093de5 Binary files /dev/null and b/docs/doxygen-user/images/solrinstall1.PNG differ diff --git a/docs/doxygen-user/images/solrinstall2.PNG b/docs/doxygen-user/images/solrinstall2.PNG new file mode 100644 index 0000000000..b0cb95d496 Binary files /dev/null and b/docs/doxygen-user/images/solrinstall2.PNG differ diff --git a/docs/doxygen-user/images/solrinstall3.PNG b/docs/doxygen-user/images/solrinstall3.PNG new file mode 100644 index 0000000000..542cfad3d5 Binary files /dev/null and b/docs/doxygen-user/images/solrinstall3.PNG differ diff --git a/docs/doxygen-user/images/symlinkjava.PNG b/docs/doxygen-user/images/symlinkjava.PNG new file mode 100644 index 0000000000..25876c7144 Binary files /dev/null and b/docs/doxygen-user/images/symlinkjava.PNG differ diff --git a/docs/doxygen-user/images/threadcount.PNG b/docs/doxygen-user/images/threadcount.PNG index c465a4a3e8..5320f6b626 100755 Binary files a/docs/doxygen-user/images/threadcount.PNG and b/docs/doxygen-user/images/threadcount.PNG differ diff --git a/docs/doxygen-user/images/tickTime.PNG b/docs/doxygen-user/images/tickTime.PNG new file mode 100755 index 0000000000..554c0b62d9 Binary files /dev/null and b/docs/doxygen-user/images/tickTime.PNG differ diff --git a/docs/doxygen-user/images/transientcache.PNG b/docs/doxygen-user/images/transientcache.PNG new file mode 100644 index 0000000000..27aa656c9c Binary files /dev/null and b/docs/doxygen-user/images/transientcache.PNG differ diff --git a/docs/doxygen-user/images/updatedServiceInstall.PNG b/docs/doxygen-user/images/updatedServiceInstall.PNG new file mode 100755 index 0000000000..7be7fb5074 Binary files /dev/null and b/docs/doxygen-user/images/updatedServiceInstall.PNG differ diff --git a/docs/doxygen-user/images/users.properties.after.PNG b/docs/doxygen-user/images/users.properties.after.PNG new file mode 100755 index 0000000000..6fee464715 Binary files /dev/null and b/docs/doxygen-user/images/users.properties.after.PNG differ diff --git a/docs/doxygen-user/images/users.properties.before.PNG b/docs/doxygen-user/images/users.properties.before.PNG new file mode 100755 index 0000000000..8dff55d79b Binary files /dev/null and b/docs/doxygen-user/images/users.properties.before.PNG differ diff --git a/docs/doxygen-user/images/wherejava.PNG b/docs/doxygen-user/images/wherejava.PNG new file mode 100644 index 0000000000..c9560e1ad8 Binary files /dev/null and b/docs/doxygen-user/images/wherejava.PNG differ diff --git a/docs/doxygen-user/installActiveMQ.dox b/docs/doxygen-user/installActiveMQ.dox new file mode 100755 index 0000000000..2b6d9c7053 --- /dev/null +++ b/docs/doxygen-user/installActiveMQ.dox @@ -0,0 +1,107 @@ +/*! \page install_activemq Install and Configure ActiveMQ +To install ActiveMQ, perform the following steps: +1. You need a 32-bit or 64-bit version of the Java Runtime Environment (JRE) installed, depending upon the version of Autopsy you have installed. You can test this by running _where java_ from the command line. If you see output like the yellow results below, you have a JRE. +

+\image html wherejava.PNG +

+You can test if you have a 32-bit or 64-bit JRE installed via the following: + - _java -d32 -version_     for a 32-bit JRE + - _java -d64 -version_     for a 64-bit JRE +

+The screenshot below shows that there is a no 32-bit JRE on this machine, and there is a 64-bit JRE. +

+\image html JRE_bitness.PNG +

+If you do not have a JRE installed, proceed to step 2. If you have a JRE installed, proceed to step 3. +

+2. Install the appropriate 32 or 64-bit version of the JRE, depending upon the version of Autopsy you have installed. Download one from: http://www.oracle.com/technetwork/java/javase/downloads/jre8-downloads-2133155.html. If you installed 32-bit Autopsy, be sure to select a package that has "x86" in the name. If you installed 64-bit Autopsy, be sure to select a package that has "x64" in the name. Follow the installation prompts to install the JRE. +3. Download ActiveMQ-5.11.1 from: http://activemq.apache.org/activemq-5111-release.html +4. Extract the files in the archive +5. Edit apache-activemq-5.11.1\\conf\\activemq.xml to add "&wireFormat.maxInactivityDuration=0" to the URI for the _transportConnector_ named _openwire_. Add the text highlighted in yellow below: +

+\image html maxinactivityduration.PNG +

+6. Move the apache-activemq-5.11.1 folder to a location of your choice, bearing in mind that the files should be in a location that the running process will have write permissions to the folder. A typical folder choice is C:\\Program Files\\apache-activemq-5.11.1. Typically, it will ask for administrator permission to move the folder. Allow it if required. +7. Install ActiveMQ as a service by navigating to the folder apache-activemq-5.11.1-bin\\apache-activemq-5.11.1\\bin\\win64, right-clicking _InstallService.bat_, clicking _Run as administrator_, then click _Yes_. +8. If you desire authentication for your ActiveMQ server (a good idea), the following directions allow you to set up credentials: + + Copy and paste the following text to the file "C:\Program Files\apache-activemq-5.11.1-bin\apache-activemq-5.11.1\conf\groups.properties", overwriting the text highlighted in yellow in the screenshot below: + + admins=system,sslclient,client,broker1,broker2 + tempDestinationAdmins=system,user,sslclient,client,broker1,broker2 + users=system,user,sslclient,client,broker1,broker2 + guests=guest + +

+ \image html groups.properties.before.PNG +

+ When complete, the file should look like this: +

+ \image html groups.properties.after.PNG +

+ + + Copy and paste the following text to the file "C:\Program Files\apache-activemq-5.11.1-bin\apache-activemq-5.11.1\conf\users.properties", overwriting the text highlighted in yellow in the screenshot below: + + system=manager + user=password + guest=password + sslclient=CN=localhost, OU=activemq.org, O=activemq.org, L=LA, ST=CA, C=US + +

+ \image html users.properties.before.PNG +

+ When complete, the file should look like this: +

+ \image html users.properties.after.PNG +

+ + + Copy and paste the following text to the file "C:\Program Files\apache-activemq-5.11.1-bin\apache-activemq-5.11.1\conf\activemq.xml", inserting the text at the line shown in yellow in the screenshot below. + + + + + + + + + + + + +

+ \image html insertTextHere.PNG +

+ After insertion, the file should look like the screenshot below, with the inserted portion highlighted in yellow. This is where you can change the username and password for your ActiveMQ setup. +

+ \image html insertedText.PNG +

+ To add a new user or change the password: + + Stop the ActiveMQ service by pressing _Start_, type _services.msc_, and press _Enter_. Find _ActiveMQ_ in the list and press the _Stop the service_ link. +
+
+ \image html StopActiveMQService.PNG +
+
+ + Edit "C:\Program Files\apache-activemq-5.11.1-bin\apache-activemq-5.11.1\conf\activemq.xml" adding the desired line. Both _username_ and _password_ are case sensitive. You will very likely want to keep your new users in the _users_ group. +
+
+ \image html newUserAndPassword.PNG +
+
+ + Start the ActiveMQ service by pressing _Start_, type _services.msc_, and press _Enter_. Find _ActiveMQ_ in the list and press the _Start the service_ link. +
+
+ \image html StartActiveMQService.PNG +
+
+ +9. If not already started, start the ActiveMQ service by pressing _Start_, type _services.msc_, and press _Enter_. Find _ActiveMQ_ in the list and press the _Start the service_ link. +10. ActiveMQ should now be fully installed and configured. You can access the admin pages in your web browser via a URL like this (set your host): http://localhost:8161/admin. The default administrator username is _admin_ with a password of _admin_ and the default regular username is _user_ with a default password of _password_. You can change these passwords by editing the file "C:\Program Files\apache-activemq-5.11.1-bin\apache-activemq-5.11.1\conf\activemq.xml" on the ActiveMQ server as discussed above. If you can see a page that looks like the following, it is ready to function. +

+\image html activemq.PNG +
+
+ If you do not see a screen like the above screenshot and you have double checked that the ActiveMQ service is running, contact your network administrator. For the ActiveMQ service to be accessible by network clients you may need to configure your Windows firewall (and any other 3rd party firewall in use) to allow communication. +

+ + +*/ diff --git a/docs/doxygen-user/installPostgres.dox b/docs/doxygen-user/installPostgres.dox new file mode 100755 index 0000000000..b71ce6f1b7 --- /dev/null +++ b/docs/doxygen-user/installPostgres.dox @@ -0,0 +1,104 @@ +/*! \page install_postgresql Install and Configure PostgreSQL +To install PostgreSQL, perform the following steps: + +1. Download a 64-bit PostgreSQL version 9.4.1 installer from http://www.enterprisedb.com/products-services-training/pgdownload#windows Choose the one that says _Win X86-64_. + +2. Run _postgresql-9.4.4-1-windows-x64.exe_ + +3. You may accept defaults for all items except for the password as you work through the wizard. Do not lose the password you enter in. This is the PostgreSQL administrator login password. + +4. You do not need to launch the StackBuilder nor acquire any more software from it. Uncheck the option to use StackBuilder and press _Finish_. + +5. Create a regular user account to use while running Autopsy. You can do this with either of two methods, graphically, or command line. We cover graphically first. + + - Graphically: + - Using the PostgreSQL administrator login and the pgAdmin III tool, create a regular user account to use while running Autopsy. + - Right click on "Login Roles" and select "New Login Role..." as shown below: +

+\image html pgAdmin.PNG +

+ - Enter the user name you would like to use in the "Role name" field. +

+\image html newLoginRole.PNG +

+ - Enter the password on the "Definition" tab. +

+\image html newPassword.PNG +

+ - Check "Can create databases" on the "Role Privileges" tab. +

+\image html newRights.PNG +

+ - Click "OK". +

+ - Command line: +
+ Use the _psql_ tool. To start _psql_, press _Start_, type _psql_, and press _Enter_ a few times until it prompts you for a password. Type in the password you gave it when installing PostgreSQL. You should see a prompt that looks like the screenshot below. +

+\image html postgresqlinstall1.PNG +

+If you want your user account name to be "Autopsy" and your password to be "myPassword", use the following command to create a new user, noting that the password is enclosed in single quotes, __not backticks nor double quotes__. Also note that it is important to type this command in from the keyboard directly, as copying and pasting can sometimes yield different characters for single quotes that can confuse _psql_. +

+The command is: +
+> CREATE    USER    Autopsy    WITH    PASSWORD    'myPassword'    CREATEDB; +
+When you see the _CREATE ROLE_ output as shown in the screenshot below, the new user has been created. You can close the _psql_ window now. +
+
+\image html postgresqlinstall2.PNG +
+ +6. Edit C:\\Program Files\\PostgreSQL\\9.4\\data\\pg_hba.conf to add an entry to allow external computers to connect via the network. +

+First, find your machine's IPv4 address and Subnet Mask (Press _Start_, type _cmd_, type _ipconfig_ and parse the results. The IP address is shown in yellow below. +
+\image html postgresqlinstall3.PNG +
+The following is an example rule that allows all clients on the 10.10.192.x subnet to connect using md5 authentication. +
+> host      all      all      10.10.192.0/24      md5 +
+__Subnet Mask Rules of thumb:__ + - If your Subnet Mask is 255.255.0.0, your rule should look like this: A.B.0.0/16, where A is the first octet in your IP address and B is the second octet. +
+ - If your Subnet Mask is 255.255.255.0, your rule should look like this: A.B.C.0/24, where A is the first octet in your IP address, B is the second octet, and C is the third octet. +

+Add the line highlighted in yellow below, formatted with spaces between the entries, adjusting the IP address to an appropriate value as described above. +

+\image html postgresqlinstall4.PNG +
+

+If you intend to use PostgreSQL from machines on a different subnet, you need an entry in the _pg_hba.conf_ file for each subnet. +

+ +7. Uncomment the following entires in the configuration file located at C:\\Program Files\\PostgreSQL\\9.4\\data\\postgresql.conf by removing the leading "#", and change their values "off" as shown below. +
+> fsync = off
+> synchronous_commit = off
+> full_page_writes = off
+
+Pictorially, change the following, from this: +

+\image html postgresqlinstall5.PNG +

+To this: +

+\image html postgresqlinstall6.PNG +

+Note the removal of the leading number symbol-this uncomments that entry. +

+ +8. Still in "C:\Program Files\PostgreSQL\9.4\data\postgresql.conf", find the entry named _max_connections_ and set it to the number of suggested connections for your configuration. A rule of thumb is add 100 connections for each Automated Ingest Node and 100 connections for each Reviewer node you plan to have in the network. More information is available at 5.1.1. See the screenshot below. +

+\image html maxConnections.PNG +

+ +9. Press _Start_, type _services.msc_, and press _Enter_. Select _postgresql-x64-9.4 PostgreSQL Server 9.4_ in the services list and click the link that says _Stop the service_ then click the link that says _Start the service_ as shown in the screenshot below. +

+\image html postgresqlinstall7.PNG +

+PostgreSQL should now be up and running. You can verify by using either the _pgAdmin_ tool or the _psql_ tool to connect to the database server from another machine on the network . +


+ +*/ diff --git a/docs/doxygen-user/installSolr.dox b/docs/doxygen-user/installSolr.dox new file mode 100755 index 0000000000..1d8920fda4 --- /dev/null +++ b/docs/doxygen-user/installSolr.dox @@ -0,0 +1,102 @@ +/*! \page install_solr Install and Configure Solr +A central Solr server is needed to store keyword indexes. To install Solr, perform the following steps: + +1. You need a 32-bit or 64-bit version of the Java Runtime Environment (JRE) installed, depending upon the version of Autopsy you have installed. You can test this by running _where java_ from the command line. If you see output like the yellow results below, you have a JRE. +

+\image html wherejava.PNG +

+You can test if you have a 32-bit or 64-bit JRE installed via the following: + - _java -d32 -version_     for a 32-bit JRE + - _java -d64 -version_     for a 64-bit JRE +

+The screenshot below shows that there is a no 32-bit JRE on this machine, and there is a 64-bit JRE. +

+\image html JRE_bitness.PNG +

+If you do not have a JRE installed, proceed to step 2. If you have a JRE installed, proceed to step 3. +

+2. Install the appropriate 32 or 64-bit version of the JRE, depending upon the version of Autopsy you have installed. Download one from: http://www.oracle.com/technetwork/java/javase/downloads/jre8-downloads-2133155.html. If you installed 32-bit Autopsy, be sure to select a package that has "x86" in the name. If you installed 64-bit Autopsy, be sure to select a package that has "x64" in the name. Follow the installation prompts to install the JRE. +3. Download the Apache Solr 4.10.3-0 installation package from https://bitnami.com/stack/solr/installer. The following steps will configure Solr to run using an account that will have access to the network storage. +4. Run the Bitnami installer, bitnami-solr-4.10.3-0-windows-installer.exe +5. If Windows prompts with User Account Control, click _Yes_ +6. Follow the prompts through to completion. You do not need to "Learn more about Bitnami cloud hosting" so you can clear the check box. +7. If you see an error dialog like the following, you may safely ignore it. +

+\image html apachebadmessage.PNG +
+8. When the installation completes, clear the "Launch Bitnami Apache Solr Stack Now?" checkbox and click _Finish_. +9. Stop _solrApache_ and _solrJetty_ services by pressing _Start_, typing _services.msc_, pressing _Enter_, and locating the _solrApache_ and _solrJetty_ Windows services. Select the services one at a time, and press _Stop the service_ once for each of them. If the service is already stopped and there is no _Stop the service_ available, this is okay. +10. Edit the C:\\Bitnami\\solr-4.10.3-0\\apache-solr\\scripts\\serviceinstall.bat script. You need administrator permission to change this file. The easiest way around this is to save a copy on the Desktop, edit the Desktop version, and copy the new one back over the top of the old. Windows will ask for permission to overwrite the old file; allow it. You should make the following changes to this file: +
+
+ - Add the following options in the _JvmOptions_ section of the line that begins with "C:\Bitnami\solr-4.10.3-0/apache-solr\scripts\prunsrv.exe" : + + ++JvmOptions=-DzkRun + + ++JvmOptions=-Dcollection.configName=AutopsyConfig + + ++JvmOptions=-Dbootstrap_confdir="C:\Bitnami\solr-4.10.3-0\apache-solr\solr\configsets\AutopsyConfig\conf" + - Replace the path to JavaHome with the path to your 64-bit version of the JRE. If you do not know the path, the correct _JavaHome_ path can be obtained by running the command "where java" from the Windows command line. An example is shown below. The text in yellow is what we are interested in. Do not include the "bin" folder in the path you place into the _JavaHome_ variable. A correct example of the final result will look something like this:   --JavaHome="C:\Program Files\Java\jre1.8.0_45" +

+ \image html wherejava.PNG +

+ Note that if you get something like the following when running the "where java" command, it is a symbolic link to the Java installation and you need to trace it to the proper folder as explained below. +

+ \image html symlinkjava.PNG +

+ To trace a symbolic link to the proper folder, use Windows Explorer to navigate to the path shown (C:\\ProgramData\\Oracle\\Java\\javapath for the example above), then right click on _java.exe_ and Click on _Properties_. You will see the path you should use in the _Target_ field, shown in the screenshot below. Do not include the "bin" folder in the path you place into the _JavaHome_ variable. +

+ \image html javaproperties.PNG +

+ A fully updated _serviceinstall.bat_ is shown below, with the changes marked in yellow. +

+ \image html updatedServiceInstall.PNG +

+11. Edit "C:\Bitnami\solr-4.10.3-0\apache-solr\solr\solr.xml" to set the _transientCacheSize_ to the maximum number of cases expected to be open concurrently. If you expect ten concurrent cases, the text to add is +\10\ +

+The added part is highlighted in yellow below. Ensure that it is inside the \ tag as follows: +
+\image html transientcache.PNG +
+Again you may have trouble saving to the file in the current location. If so, just save it out to the desktop and copy the edited file back over the top of the original. +

+12. Edit "C:\Bitnami\solr-4.10.3-0\apache-solr\resources/log4j.properties" to configure Solr log settings: + - Increase the log rotation size threshold (_log4j\.appender\.file\.MaxFileSize_) from 4MB to 100MB. + - Remove the _CONSOLE_ appender from the _log4j\.rootLogger_ line. +

+The log file should end up looking like this (modified lines are highlighted in yellow): +

+\image html log4j.PNG +

+13. Edit the file "C:\Bitnami\solr-4.10.3-0\apache-solr\solr\zoo.cfg" to increase the _tickTime_ value to 15000 as shown in the screenshot below. +

+\image html tickTime.PNG +

+14. Edit the file "C:\Bitnami\solr-4.10.3-0\apache-solr\solr\zoo.cfg" to set the value dataDir=C:/Bitnami/zookeeper as shown in the screenshot below. +

+\image html dataDir.PNG +

+15. Copy the folder _configsets_ from your Autopsy installation ("C:\Program Files (x86)\Autopsy-4.0\autopsy\solr\solr" for 32-bit or "C:\Program Files\Autopsy-4.0\autopsy\solr\solr" for 64-bit) to "C:\\Bitnami\\solr-4.10.3-0\\apache-solr\\solr" +16. Copy the folder _lib_ from your Autopsy installation ("C:\Program Files (x86)\Autopsy-4.0\autopsy\solr\solr" for 32-bit or "C:\Program Files\Autopsy-4.0\autopsy\solr\solr" for 64-bit) to "C:\\Bitnami\\solr-4.10.3-0\\apache-solr\\solr" +17. Right-click on the file "C:\Bitnami\solr-4.10.3-0\apache-solr\scripts\serviceinstall.bat" and click "Run As Administrator", selecting _Yes_ if prompted by User Account Control. + + If there is no "Run as administrator" option when you right-click the _serviceinstall.bat_ file, start a Windows command prompt as administrator by pressing _Start_, typing _command_, right clicking on _Command Prompt_, and clicking on _Run as administrator_. Then run the following command to install the _solrJetty_ service: +

+ cmd /c C:\\Bitnami\\solr-4.10.3-0\\apache-solr\\scripts\\serviceinstall.bat INSTALL +

+ Your command prompt should look like the screenshot below. Very likely it will say "The solrJetty service could not be started." This is okay. +

+\image html solrinstall1.PNG +

+18. Press _Start_, type _services.msc_, and press _Enter_. Find _solrJetty_. If the service is running, press _Stop the service_, then double click it, and switch to the _Log On_ tab to change the logon credentials to a user who will have access to read and write the primary shared drive. If the machine is on a domain, the Account Name will be in the form of _DOMAINNAME\\username_ as shown in the example below. Note that in the screenshot below, the domain name is _DOMAIN_ and the user name is _username_. These are just examples, not real values. +

+\image html solrinstall2.PNG +
+If the machine is on a domain, **make sure** to select the domain with the mouse by going to the _Log On_ tab, clicking _Browse_, then clicking _Locations_ and selecting the domain of interest. Then enter the user name desired and press _Check Names_. When that completes, press _OK_, type in the password once for each box and press _OK_. You may see "The user has been granted the log on as a service right." + +19. You should be able to see the Solr service in a web browser via the URL http://localhost:8983/solr/#/ as shown in the screenshot below. +

+\image html solrinstall3.PNG +

+If the service is appropriately started and you are unable to see the screenshot above, contact your network administrator to open ports in the firewall. +

+ +*/ diff --git a/docs/doxygen-user/installation.dox b/docs/doxygen-user/installation.dox index 48e6308f99..c944c805ac 100755 --- a/docs/doxygen-user/installation.dox +++ b/docs/doxygen-user/installation.dox @@ -1,34 +1,72 @@ -/*! \page installation_page Installation +/*! \page installation_page Installing Autopsy -Installation ------ +[TOC] +\section prereqs Prerequisites +It is _highly_ recommended to remove or disable any antivirus software from computers that will be processing or reviewing cases. Antivirus software will often conflict with forensic software, and may quarantine or even delete some of your results before you get a chance to look at them. +

+\section download Download Download Autopsy from the website: http://sleuthkit.org/autopsy/download.php The current version of Autopsy 3 runs only on Microsoft Windows. -We have gotten it to run on other platforms, such as Linux and OS X, but we do not have it in a state that makes it easy to distribute and find the needed libraries. +We have gotten older versions to run on other platforms, such as Linux and OS X, but we do not have it in a state that makes it easy to distribute and find the needed libraries. +

-The Windows installer is self-contained and will place everything in the needed places. Simply follow the standard prompts for installation. +\section installation_section Installation +To install Autopsy, perform the following steps: +1. Run the Autopsy _msi_ file +2. If Windows prompts with User Account Control, click _Yes_ +3. Click through the dialog boxes until you click a button that says _Finish_ +4. Core Autopsy should now be fully installed +
+\section deployment_types Deployment Types +There are two types of cases that Autopsy can create and use: + - **Standalone**: A single user with a single computer. Not intended to have multiple examiners working on the same case. + - **Collaborative**: A multi-user environment with multiple computers. Multiple examiners can work on the same case at the same time. -Optimizing Performance ------ +Both deployment types use the same analysis modules and the same base installer. +

+\subsection standalone_install Standalone (Single User) Installation +1. Install Autopsy as explained in \ref installation_section above. The Windows installer is self-contained and will place everything in the needed places. Simply follow the standard prompts for installation. +
+\subsection collab_install Collaborative (Multi-user) Installation +To use the Multi-user collaboration feature, three additional software packages are required. These packages install servers that need to be accessible to machines running Autopsy collaborative cases via the network. These servers do not have to be installed on the same machine as Autopsy, nor on the same machine as each other. + +1. Install Autopsy just as in \ref standalone_install above +2. Install and configure Apache ActiveMQ on a machine accessible to Autopsy nodes. This is explained in \ref install_activemq. +3. Install and configure Bitnami Solr on a machine accessible to Autopsy nodes. This is explained in \ref install_solr. +4. Install and configure PostgreSQL on a machine accessible to Autopsy nodes. This is explained in \ref install_postgresql. +5. Configure Multi-user settings. This is explained in \ref multi_user_options. + +
+While you may run all of the external services (ActiveMQ, Solr, and PostgreSQL) on the same machine that is running Autopsy, this is not ideal. Spreading the services out across several machines can improve throughput. Keep in mind that all the machines need to be able to communicate with each other and see the shared drive over the network. +If possible, place Solr on a machine by itself, as it is the largest RAM and CPU utilizer among the servers. +

+To use Multi-user cases, there needs to be a shared network drive accessible to all participating computers. +

+ +\section optimizing_performance Optimizing Performance After installing Autopsy, there are several hardware-based things that we suggest you do to optimize performance: - -1) Change the number of parallel pipelines used at run time. The default is two pipelines, but this can be increased if you are running on a system with several cores. To do this: -- Run Autopsy from the Start Menu or desktop -- When presented with the case creation splash screen, cancel/close the window -- Select "Tools", "Options" -- On the "Autopsy" tab, there is a drop down for "Number of thread to use for file ingest". We recommend that you set this value to be smaller than the number of cores minus two. If you set this number too high, performance can degrade because the pipelines are fighting for the same resources. Individual testing should be done to find an optimal setting. -- After each change, restart Autopsy to let this setting take effect. - +1. Change the number of parallel pipelines used at run time. The default is two pipelines, but this can be increased if you are running on a system with several cores. To do this: + - Run Autopsy from the Start Menu or desktop + - When presented with the case creation splash screen, cancel/close the window + - Select "Tools", "Options" + - On the "Autopsy" tab, there is a drop down for _Number of threads to use for file ingest_. We recommend you set this value 4. If you set this number too high, performance can degrade because the pipelines are fighting for the same physical resources. Individual testing should be done to find an optimal setting. + - After each change, restart Autopsy to let this setting take effect. +

\image html threadcount.PNG +

+2. In the screenshot above, there is an option to Enable timeout to allow modules to automatically terminate after a set amount of time. Enabling this feature by applying a checkmark and setting a number of hours puts a maximum amount of time an individual module may attempt to process before being stopped. If enabled and a module attempts to run for longer than this value, Autopsy stops the module and moves on to process the next module. This allows processing to continue even if a rogue module does not end appropriately in a reasonable amount of time. +

+3. When making a case, use different drives to store the case and the images. The case directory is where the SQLite database and keyword search index are stored in Single-user cases. This allows the maximum amount of data to be read and written at the same time. If using collaborative Multi-user mode, it is important that UNC paths are used to specifiy drive names. Fully-specified UNC paths should be in the form of \\\\hostname\\sharename\\folder. +

+4. We have had best performance using either solid state drives or fibre channel-attached SAN storage. + -2) When making a case, use different drives to store the case and the images. The case directory is where the SQLite database and keyword search index are stored. This allows the maximum amount of data to be read and written at the same time. -3) We have had best performance using either local solid state drives or fibre channel-attached SAN storage. */ diff --git a/docs/doxygen-user/main.dox b/docs/doxygen-user/main.dox index c59bc2f19b..948cf5b546 100644 --- a/docs/doxygen-user/main.dox +++ b/docs/doxygen-user/main.dox @@ -15,6 +15,11 @@ The following topics are available here: - \subpage installation_page - \subpage quick_start_guide "Quick Start Guide" - \subpage workflow_page +- Multi-user Collaboration Setup + - \subpage install_activemq + - \subpage install_postgresql + - \subpage install_solr + - \subpage multiuser_page - Cases and Adding Data Sources - \subpage cases_page - \subpage ds_page diff --git a/docs/doxygen-user/multiuser.dox b/docs/doxygen-user/multiuser.dox new file mode 100644 index 0000000000..d223c0a4a9 --- /dev/null +++ b/docs/doxygen-user/multiuser.dox @@ -0,0 +1,38 @@ +/*! \page multiuser_page Using Multi-user Collaboration + +\section creating_multi_user_cases Creating Multi-user cases +The collaborative Multi-user capabilities enable Autopsy cases to be opened by multiple reviewers at the same time allowing simultaneous case review with multiple reviewers. + + +When creating a case, users are now presented with a choice of Single-user or Multi-user as shown in the screenshot below. +

+\image html case-newcase.PNG +

+Single-user functions the same as always, with a back end SQLite database and a machine-local version of Solr. + +Multi-user allows multiple computers to open the same case at the same time. In order to accomplish this, some setup needs to take place. To use a Multi-user setup, one must install PostgreSQL, Solr, and ActiveMQ (see \subpage installation_page) on machines connected to the network, and properly configure Autopsy to interact with these services. This configuration is done in the Multi-user options panel, discussed below. +

+ +\section multi_user_options Multi-user options panel +As shown in the screenshot below, all three services need configuration of IP addresses and ports, and two of them need user names and passwords. Enter the correct information into the dialog and press okay. Note that in order to create or open Multi-user cases, "Enable Multi-user cases" must be checked and the settings below must be correct. +

+\image html multiuser_settings.PNG +

+Other Multi-user Information +======= + - When using a case in collaborative Multi-user mode, other nodes could be running data ingest. While this is happening, you will see a progress bar labelled with the hostname of the machine performing the ingest on the bottom right of Autopsy. The progress bar will continue to move back and forth until ingest has been completed or cancelled. You can still run ingest on your local machine while this is ongoing. This is shown in the screenshot below. +

+\image html othernodeingesting.PNG +

+ - When issues occur, there is an information "bubble" on the bottom right of the screen. It has an "i" inside a circle, with the color of the circle changed based upon the message. It uses red for bad and blue for good. See the screenshot below. +

+\image html messagebubbles.PNG +

+ - Clicking on the information "bubble" brings up the list of prior notifications that have not been dismissed by clicking on the "x". As you can see in the screenshot below, the network cable was unplugged from the machine and it lost all connection to the three services. When the cable was reconnected, it found the services again. +

+\image html messagebubblesbigger.PNG + +

+ - While using collaborative Multi-user mode, it is important that UNC paths are used to specifiy drive names. Drive mapping will work, but it is sometimes difficult to get all the machines participating in a case to map to the same drive letters for the same resources. It is much simpler to use fully-specified UNC paths in the form of \\\\hostname\\sharename\\folder. + +*/ diff --git a/docs/doxygen/Doxyfile b/docs/doxygen/Doxyfile index e8701f1831..ef6d9e0c2a 100755 --- a/docs/doxygen/Doxyfile +++ b/docs/doxygen/Doxyfile @@ -1,110 +1,129 @@ -# Doxyfile 1.8.2 +# Doxyfile 1.8.9.1 # This file describes the settings to be used by the documentation system # doxygen (www.doxygen.org) for a project. # -# All text after a hash (#) is considered a comment and will be ignored. +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. # The format is: -# TAG = value [value, ...] -# For lists items can also be appended using: -# TAG += value [value, ...] -# Values that contain spaces should be placed between quotes (" "). +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). #--------------------------------------------------------------------------- # Project related configuration options #--------------------------------------------------------------------------- # This tag specifies the encoding used for all characters in the config file -# that follow. The default is UTF-8 which is also the encoding used for all -# text before the first occurrence of this tag. Doxygen uses libiconv (or the -# iconv built into libc) for the transcoding. See -# http://www.gnu.org/software/libiconv for the list of possible encodings. +# that follow. The default is UTF-8 which is also the encoding used for all text +# before the first occurrence of this tag. Doxygen uses libiconv (or the iconv +# built into libc) for the transcoding. See http://www.gnu.org/software/libiconv +# for the list of possible encodings. +# The default value is: UTF-8. DOXYFILE_ENCODING = UTF-8 -# The PROJECT_NAME tag is a single word (or sequence of words) that should -# identify the project. Note that if you do not use Doxywizard you need -# to put quotes around the project name if it contains spaces. +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. PROJECT_NAME = "Autopsy" -# The PROJECT_NUMBER tag can be used to enter a project or revision number. -# This could be handy for archiving the generated documentation or -# if some version control system is used. +# The PROJECT_NUMBER tag can be used to enter a project or revision number. This +# could be handy for archiving the generated documentation or if some version +# control system is used. PROJECT_NUMBER = 3.1 # Using the PROJECT_BRIEF tag one can provide an optional one line description -# for a project that appears at the top of each page and should give viewer -# a quick idea about the purpose of the project. Keep the description short. +# for a project that appears at the top of each page and should give viewer a +# quick idea about the purpose of the project. Keep the description short. PROJECT_BRIEF = "Graphical digital forensics platform for The Sleuth Kit and other tools." -# With the PROJECT_LOGO tag one can specify an logo or icon that is -# included in the documentation. The maximum height of the logo should not -# exceed 55 pixels and the maximum width should not exceed 200 pixels. -# Doxygen will copy the logo to the output directory. +# With the PROJECT_LOGO tag one can specify a logo or an icon that is included +# in the documentation. The maximum height of the logo should not exceed 55 +# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy +# the logo to the output directory. PROJECT_LOGO = -# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) -# base path where the generated documentation will be put. -# If a relative path is entered, it will be relative to the location -# where doxygen was started. If left blank the current directory will be used. +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path +# into which the generated documentation will be written. If a relative path is +# entered, it will be relative to the location where doxygen was started. If +# left blank the current directory will be used. -OUTPUT_DIRECTORY = doxygen_docs +OUTPUT_DIRECTORY = doxygen_docs/ -# If the CREATE_SUBDIRS tag is set to YES, then doxygen will create -# 4096 sub-directories (in 2 levels) under the output directory of each output -# format and will distribute the generated files over these directories. -# Enabling this option can be useful when feeding doxygen a huge amount of -# source files, where putting all generated files in the same directory would -# otherwise cause performance problems for the file system. +# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- +# directories (in 2 levels) under the output directory of each output format and +# will distribute the generated files over these directories. Enabling this +# option can be useful when feeding doxygen a huge amount of source files, where +# putting all generated files in the same directory would otherwise causes +# performance problems for the file system. +# The default value is: NO. CREATE_SUBDIRS = NO +# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII +# characters to appear in the names of generated files. If set to NO, non-ASCII +# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode +# U+3044. +# The default value is: NO. + +ALLOW_UNICODE_NAMES = NO + # The OUTPUT_LANGUAGE tag is used to specify the language in which all # documentation generated by doxygen is written. Doxygen will use this # information to generate all constant output in the proper language. -# The default language is English, other supported languages are: -# Afrikaans, Arabic, Brazilian, Catalan, Chinese, Chinese-Traditional, -# Croatian, Czech, Danish, Dutch, Esperanto, Farsi, Finnish, French, German, -# Greek, Hungarian, Italian, Japanese, Japanese-en (Japanese with English -# messages), Korean, Korean-en, Lithuanian, Norwegian, Macedonian, Persian, -# Polish, Portuguese, Romanian, Russian, Serbian, Serbian-Cyrillic, Slovak, -# Slovene, Spanish, Swedish, Ukrainian, and Vietnamese. +# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, +# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), +# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, +# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), +# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, +# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, +# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, +# Ukrainian and Vietnamese. +# The default value is: English. OUTPUT_LANGUAGE = English -# If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will -# include brief member descriptions after the members that are listed in -# the file and class documentation (similar to JavaDoc). -# Set to NO to disable this. +# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member +# descriptions after the members that are listed in the file and class +# documentation (similar to Javadoc). Set to NO to disable this. +# The default value is: YES. BRIEF_MEMBER_DESC = YES -# If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend -# the brief description of a member or function before the detailed description. -# Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief +# description of a member or function before the detailed description +# +# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the # brief descriptions will be completely suppressed. +# The default value is: YES. REPEAT_BRIEF = YES -# This tag implements a quasi-intelligent brief description abbreviator -# that is used to form the text in various listings. Each string -# in this list, if found as the leading text of the brief description, will be -# stripped from the text and the result after processing the whole list, is -# used as the annotated text. Otherwise, the brief description is used as-is. -# If left blank, the following values are used ("$name" is automatically -# replaced with the name of the entity): "The $name class" "The $name widget" -# "The $name file" "is" "provides" "specifies" "contains" -# "represents" "a" "an" "the" +# This tag implements a quasi-intelligent brief description abbreviator that is +# used to form the text in various listings. Each string in this list, if found +# as the leading text of the brief description, will be stripped from the text +# and the result, after processing the whole list, is used as the annotated +# text. Otherwise, the brief description is used as-is. If left blank, the +# following values are used ($name is automatically replaced with the name of +# the entity):The $name class, The $name widget, The $name file, is, provides, +# specifies, contains, represents, a, an and the. ABBREVIATE_BRIEF = # If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then -# Doxygen will generate a detailed section even if there is only a brief +# doxygen will generate a detailed section even if there is only a brief # description. +# The default value is: NO. ALWAYS_DETAILED_SEC = NO @@ -112,242 +131,269 @@ ALWAYS_DETAILED_SEC = NO # inherited members of a class in the documentation of that class as if those # members were ordinary class members. Constructors, destructors and assignment # operators of the base classes will not be shown. +# The default value is: NO. INLINE_INHERITED_MEMB = YES -# If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full -# path before files name in the file list and in the header files. If set -# to NO the shortest path that makes the file name unique will be used. +# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path +# before files name in the file list and in the header files. If set to NO the +# shortest path that makes the file name unique will be used +# The default value is: YES. FULL_PATH_NAMES = YES -# If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag -# can be used to strip a user-defined part of the path. Stripping is -# only done if one of the specified strings matches the left-hand part of -# the path. The tag can be used to show relative paths in the file list. -# If left blank the directory from which doxygen is run is used as the -# path to strip. Note that you specify absolute paths here, but also -# relative paths, which will be relative from the directory where doxygen is -# started. +# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. +# Stripping is only done if one of the specified strings matches the left-hand +# part of the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the path to +# strip. +# +# Note that you can specify absolute paths here, but also relative paths, which +# will be relative from the directory where doxygen is started. +# This tag requires that the tag FULL_PATH_NAMES is set to YES. STRIP_FROM_PATH = -# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of -# the path mentioned in the documentation of a class, which tells -# the reader which header file to include in order to use a class. -# If left blank only the name of the header file containing the class -# definition is used. Otherwise one should specify the include paths that -# are normally passed to the compiler using the -I flag. +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the +# path mentioned in the documentation of a class, which tells the reader which +# header file to include in order to use a class. If left blank only the name of +# the header file containing the class definition is used. Otherwise one should +# specify the list of include paths that are normally passed to the compiler +# using the -I flag. STRIP_FROM_INC_PATH = -# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter -# (but less readable) file names. This can be useful if your file system -# doesn't support long names like on DOS, Mac, or CD-ROM. +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but +# less readable) file names. This can be useful is your file systems doesn't +# support long names like on DOS, Mac, or CD-ROM. +# The default value is: NO. SHORT_NAMES = NO -# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen -# will interpret the first line (until the first dot) of a JavaDoc-style -# comment as the brief description. If set to NO, the JavaDoc -# comments will behave just like regular Qt-style comments -# (thus requiring an explicit @brief command for a brief description.) +# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the +# first line (until the first dot) of a Javadoc-style comment as the brief +# description. If set to NO, the Javadoc-style will behave just like regular Qt- +# style comments (thus requiring an explicit @brief command for a brief +# description.) +# The default value is: NO. JAVADOC_AUTOBRIEF = NO -# If the QT_AUTOBRIEF tag is set to YES then Doxygen will -# interpret the first line (until the first dot) of a Qt-style -# comment as the brief description. If set to NO, the comments -# will behave just like regular Qt-style comments (thus requiring -# an explicit \brief command for a brief description.) +# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first +# line (until the first dot) of a Qt-style comment as the brief description. If +# set to NO, the Qt-style will behave just like regular Qt-style comments (thus +# requiring an explicit \brief command for a brief description.) +# The default value is: NO. QT_AUTOBRIEF = NO -# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen -# treat a multi-line C++ special comment block (i.e. a block of //! or /// -# comments) as a brief description. This used to be the default behaviour. -# The new default is to treat a multi-line C++ comment block as a detailed -# description. Set this tag to YES if you prefer the old behaviour instead. +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a +# multi-line C++ special comment block (i.e. a block of //! or /// comments) as +# a brief description. This used to be the default behavior. The new default is +# to treat a multi-line C++ comment block as a detailed description. Set this +# tag to YES if you prefer the old behavior instead. +# +# Note that setting this tag to YES also means that rational rose comments are +# not recognized any more. +# The default value is: NO. MULTILINE_CPP_IS_BRIEF = NO -# If the INHERIT_DOCS tag is set to YES (the default) then an undocumented -# member inherits the documentation from any documented member that it -# re-implements. +# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the +# documentation from any documented member that it re-implements. +# The default value is: YES. INHERIT_DOCS = YES -# If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce -# a new page for each member. If set to NO, the documentation of a member will -# be part of the file/class/namespace that contains it. +# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new +# page for each member. If set to NO, the documentation of a member will be part +# of the file/class/namespace that contains it. +# The default value is: NO. SEPARATE_MEMBER_PAGES = NO -# The TAB_SIZE tag can be used to set the number of spaces in a tab. -# Doxygen uses this value to replace tabs by spaces in code fragments. +# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen +# uses this value to replace tabs by spaces in code fragments. +# Minimum value: 1, maximum value: 16, default value: 4. TAB_SIZE = 8 -# This tag can be used to specify a number of aliases that acts -# as commands in the documentation. An alias has the form "name=value". -# For example adding "sideeffect=\par Side Effects:\n" will allow you to -# put the command \sideeffect (or @sideeffect) in the documentation, which -# will result in a user-defined paragraph with heading "Side Effects:". -# You can put \n's in the value part of an alias to insert newlines. +# This tag can be used to specify a number of aliases that act as commands in +# the documentation. An alias has the form: +# name=value +# For example adding +# "sideeffect=@par Side Effects:\n" +# will allow you to put the command \sideeffect (or @sideeffect) in the +# documentation, which will result in a user-defined paragraph with heading +# "Side Effects:". You can put \n's in the value part of an alias to insert +# newlines. ALIASES = # This tag can be used to specify a number of word-keyword mappings (TCL only). -# A mapping has the form "name=value". For example adding -# "class=itcl::class" will allow you to use the command class in the -# itcl::class meaning. +# A mapping has the form "name=value". For example adding "class=itcl::class" +# will allow you to use the command class in the itcl::class meaning. TCL_SUBST = -# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C -# sources only. Doxygen will then generate output that is more tailored for C. -# For instance, some of the names that are used will be different. The list -# of all members will be omitted, etc. +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources +# only. Doxygen will then generate output that is more tailored for C. For +# instance, some of the names that are used will be different. The list of all +# members will be omitted, etc. +# The default value is: NO. OPTIMIZE_OUTPUT_FOR_C = NO -# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java -# sources only. Doxygen will then generate output that is more tailored for -# Java. For instance, namespaces will be presented as packages, qualified -# scopes will look different, etc. +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or +# Python sources only. Doxygen will then generate output that is more tailored +# for that language. For instance, namespaces will be presented as packages, +# qualified scopes will look different, etc. +# The default value is: NO. OPTIMIZE_OUTPUT_JAVA = YES # Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran -# sources only. Doxygen will then generate output that is more tailored for -# Fortran. +# sources. Doxygen will then generate output that is tailored for Fortran. +# The default value is: NO. OPTIMIZE_FOR_FORTRAN = NO # Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL -# sources. Doxygen will then generate output that is tailored for -# VHDL. +# sources. Doxygen will then generate output that is tailored for VHDL. +# The default value is: NO. OPTIMIZE_OUTPUT_VHDL = NO # Doxygen selects the parser to use depending on the extension of the files it # parses. With this tag you can assign which parser to use for a given # extension. Doxygen has a built-in mapping, but you can override or extend it -# using this tag. The format is ext=language, where ext is a file extension, -# and language is one of the parsers supported by doxygen: IDL, Java, -# Javascript, CSharp, C, C++, D, PHP, Objective-C, Python, Fortran, VHDL, C, -# C++. For instance to make doxygen treat .inc files as Fortran files (default -# is PHP), and .f files as C (default is Fortran), use: inc=Fortran f=C. Note -# that for custom extensions you also need to set FILE_PATTERNS otherwise the -# files are not read by doxygen. +# using this tag. The format is ext=language, where ext is a file extension, and +# language is one of the parsers supported by doxygen: IDL, Java, Javascript, +# C#, C, C++, D, PHP, Objective-C, Python, Fortran (fixed format Fortran: +# FortranFixed, free formatted Fortran: FortranFree, unknown formatted Fortran: +# Fortran. In the later case the parser tries to guess whether the code is fixed +# or free formatted code, this is the default for Fortran type files), VHDL. For +# instance to make doxygen treat .inc files as Fortran files (default is PHP), +# and .f files as C (default is Fortran), use: inc=Fortran f=C. +# +# Note: For files without extension you can use no_extension as a placeholder. +# +# Note that for custom extensions you also need to set FILE_PATTERNS otherwise +# the files are not read by doxygen. EXTENSION_MAPPING = -# If MARKDOWN_SUPPORT is enabled (the default) then doxygen pre-processes all -# comments according to the Markdown format, which allows for more readable +# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments +# according to the Markdown format, which allows for more readable # documentation. See http://daringfireball.net/projects/markdown/ for details. -# The output of markdown processing is further processed by doxygen, so you -# can mix doxygen, HTML, and XML commands with Markdown formatting. -# Disable only in case of backward compatibilities issues. +# The output of markdown processing is further processed by doxygen, so you can +# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in +# case of backward compatibilities issues. +# The default value is: YES. MARKDOWN_SUPPORT = YES -# When enabled doxygen tries to link words that correspond to documented classes, -# or namespaces to their corresponding documentation. Such a link can be -# prevented in individual cases by by putting a % sign in front of the word or +# When enabled doxygen tries to link words that correspond to documented +# classes, or namespaces to their corresponding documentation. Such a link can +# be prevented in individual cases by putting a % sign in front of the word or # globally by setting AUTOLINK_SUPPORT to NO. +# The default value is: YES. AUTOLINK_SUPPORT = YES # If you use STL classes (i.e. std::string, std::vector, etc.) but do not want -# to include (a tag file for) the STL sources as input, then you should -# set this tag to YES in order to let doxygen match functions declarations and -# definitions whose arguments contain STL classes (e.g. func(std::string); v.s. -# func(std::string) {}). This also makes the inheritance and collaboration +# to include (a tag file for) the STL sources as input, then you should set this +# tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); +# versus func(std::string) {}). This also make the inheritance and collaboration # diagrams that involve STL classes more complete and accurate. +# The default value is: NO. BUILTIN_STL_SUPPORT = NO # If you use Microsoft's C++/CLI language, you should set this option to YES to # enable parsing support. +# The default value is: NO. CPP_CLI_SUPPORT = NO -# Set the SIP_SUPPORT tag to YES if your project consists of sip sources only. -# Doxygen will parse them like normal C++ but will assume all classes use public -# instead of private inheritance when no explicit protection keyword is present. +# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: +# http://www.riverbankcomputing.co.uk/software/sip/intro) sources only. Doxygen +# will parse them like normal C++ but will assume all classes use public instead +# of private inheritance when no explicit protection keyword is present. +# The default value is: NO. SIP_SUPPORT = NO -# For Microsoft's IDL there are propget and propput attributes to indicate getter and setter methods for a property. Setting this option to YES (the default) will make doxygen replace the get and set methods by a property in the documentation. This will only work if the methods are indeed getting or setting a simple type. If this is not the case, or you want to show the methods anyway, you should set this option to NO. +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES will make +# doxygen to replace the get and set methods by a property in the documentation. +# This will only work if the methods are indeed getting or setting a simple +# type. If this is not the case, or you want to show the methods anyway, you +# should set this option to NO. +# The default value is: YES. IDL_PROPERTY_SUPPORT = YES # If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC -# tag is set to YES, then doxygen will reuse the documentation of the first +# tag is set to YES then doxygen will reuse the documentation of the first # member in the group (if any) for the other members of the group. By default # all members of a group must be documented explicitly. +# The default value is: NO. DISTRIBUTE_GROUP_DOC = NO -# Set the SUBGROUPING tag to YES (the default) to allow class member groups of -# the same type (for instance a group of public functions) to be put as a -# subgroup of that type (e.g. under the Public Functions section). Set it to -# NO to prevent subgrouping. Alternatively, this can be done per class using -# the \nosubgrouping command. +# Set the SUBGROUPING tag to YES to allow class member groups of the same type +# (for instance a group of public functions) to be put as a subgroup of that +# type (e.g. under the Public Functions section). Set it to NO to prevent +# subgrouping. Alternatively, this can be done per class using the +# \nosubgrouping command. +# The default value is: YES. SUBGROUPING = YES -# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and -# unions are shown inside the group in which they are included (e.g. using -# @ingroup) instead of on a separate page (for HTML and Man pages) or -# section (for LaTeX and RTF). +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions +# are shown inside the group in which they are included (e.g. using \ingroup) +# instead of on a separate page (for HTML and Man pages) or section (for LaTeX +# and RTF). +# +# Note that this feature does not work in combination with +# SEPARATE_MEMBER_PAGES. +# The default value is: NO. INLINE_GROUPED_CLASSES = NO -# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and -# unions with only public data fields will be shown inline in the documentation -# of the scope in which they are defined (i.e. file, namespace, or group -# documentation), provided this scope is documented. If set to NO (the default), -# structs, classes, and unions are shown on a separate page (for HTML and Man -# pages) or section (for LaTeX and RTF). +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions +# with only public data fields or simple typedef fields will be shown inline in +# the documentation of the scope in which they are defined (i.e. file, +# namespace, or group documentation), provided this scope is documented. If set +# to NO, structs, classes, and unions are shown on a separate page (for HTML and +# Man pages) or section (for LaTeX and RTF). +# The default value is: NO. INLINE_SIMPLE_STRUCTS = NO -# When TYPEDEF_HIDES_STRUCT is enabled, a typedef of a struct, union, or enum -# is documented as struct, union, or enum with the name of the typedef. So +# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or +# enum is documented as struct, union, or enum with the name of the typedef. So # typedef struct TypeS {} TypeT, will appear in the documentation as a struct # with name TypeT. When disabled the typedef will appear as a member of a file, -# namespace, or class. And the struct will be named TypeS. This can typically -# be useful for C code in case the coding convention dictates that all compound +# namespace, or class. And the struct will be named TypeS. This can typically be +# useful for C code in case the coding convention dictates that all compound # types are typedef'ed and only the typedef is referenced, never the tag name. +# The default value is: NO. TYPEDEF_HIDES_STRUCT = NO -# The SYMBOL_CACHE_SIZE determines the size of the internal cache use to -# determine which symbols to keep in memory and which to flush to disk. -# When the cache is full, less often used symbols will be written to disk. -# For small to medium size projects (<1000 input files) the default value is -# probably good enough. For larger projects a too small cache size can cause -# doxygen to be busy swapping symbols to and from disk most of the time -# causing a significant performance penalty. -# If the system has enough physical memory increasing the cache will improve the -# performance by keeping more symbols in memory. Note that the value works on -# a logarithmic scale so increasing the size by one will roughly double the -# memory usage. The cache size is given by this formula: -# 2^(16+SYMBOL_CACHE_SIZE). The valid range is 0..9, the default is 0, -# corresponding to a cache size of 2^16 = 65536 symbols. - -SYMBOL_CACHE_SIZE = 0 - -# Similar to the SYMBOL_CACHE_SIZE the size of the symbol lookup cache can be -# set using LOOKUP_CACHE_SIZE. This cache is used to resolve symbols given -# their name and scope. Since this can be an expensive process and often the -# same symbol appear multiple times in the code, doxygen keeps a cache of -# pre-resolved symbols. If the cache is too small doxygen will become slower. -# If the cache is too large, memory is wasted. The cache size is given by this -# formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range is 0..9, the default is 0, -# corresponding to a cache size of 2^16 = 65536 symbols. +# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This +# cache is used to resolve symbols given their name and scope. Since this can be +# an expensive process and often the same symbol appears multiple times in the +# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small +# doxygen will become slower. If the cache is too large, memory is wasted. The +# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range +# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 +# symbols. At the end of a run doxygen will report the cache usage and suggest +# the optimal cache size from a speed point of view. +# Minimum value: 0, maximum value: 9, default value: 0. LOOKUP_CACHE_SIZE = 0 @@ -355,311 +401,362 @@ LOOKUP_CACHE_SIZE = 0 # Build related configuration options #--------------------------------------------------------------------------- -# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in -# documentation are documented, even if no documentation was available. -# Private class members and static file members will be hidden unless -# the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES +# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in +# documentation are documented, even if no documentation was available. Private +# class members and static file members will be hidden unless the +# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. +# Note: This will also disable the warnings about undocumented members that are +# normally produced when WARNINGS is set to YES. +# The default value is: NO. EXTRACT_ALL = YES -# If the EXTRACT_PRIVATE tag is set to YES all private members of a class -# will be included in the documentation. +# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will +# be included in the documentation. +# The default value is: NO. EXTRACT_PRIVATE = YES -# If the EXTRACT_PACKAGE tag is set to YES all members with package or internal +# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal # scope will be included in the documentation. +# The default value is: NO. EXTRACT_PACKAGE = NO -# If the EXTRACT_STATIC tag is set to YES all static members of a file -# will be included in the documentation. +# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be +# included in the documentation. +# The default value is: NO. EXTRACT_STATIC = YES -# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) -# defined locally in source files will be included in the documentation. -# If set to NO only classes defined in header files are included. +# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined +# locally in source files will be included in the documentation. If set to NO, +# only classes defined in header files are included. Does not have any effect +# for Java sources. +# The default value is: YES. EXTRACT_LOCAL_CLASSES = YES -# This flag is only useful for Objective-C code. When set to YES local -# methods, which are defined in the implementation section but not in -# the interface are included in the documentation. -# If set to NO (the default) only methods in the interface are included. +# This flag is only useful for Objective-C code. If set to YES, local methods, +# which are defined in the implementation section but not in the interface are +# included in the documentation. If set to NO, only methods in the interface are +# included. +# The default value is: NO. EXTRACT_LOCAL_METHODS = NO # If this flag is set to YES, the members of anonymous namespaces will be # extracted and appear in the documentation as a namespace called -# 'anonymous_namespace{file}', where file will be replaced with the base -# name of the file that contains the anonymous namespace. By default -# anonymous namespaces are hidden. +# 'anonymous_namespace{file}', where file will be replaced with the base name of +# the file that contains the anonymous namespace. By default anonymous namespace +# are hidden. +# The default value is: NO. EXTRACT_ANON_NSPACES = NO -# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all -# undocumented members of documented classes, files or namespaces. -# If set to NO (the default) these members will be included in the -# various overviews, but no documentation section is generated. -# This option has no effect if EXTRACT_ALL is enabled. +# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all +# undocumented members inside documented classes or files. If set to NO these +# members will be included in the various overviews, but no documentation +# section is generated. This option has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. HIDE_UNDOC_MEMBERS = NO -# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all -# undocumented classes that are normally visible in the class hierarchy. -# If set to NO (the default) these classes will be included in the various -# overviews. This option has no effect if EXTRACT_ALL is enabled. +# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. If set +# to NO, these classes will be included in the various overviews. This option +# has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. HIDE_UNDOC_CLASSES = NO -# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all -# friend (class|struct|union) declarations. -# If set to NO (the default) these declarations will be included in the -# documentation. +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend +# (class|struct|union) declarations. If set to NO, these declarations will be +# included in the documentation. +# The default value is: NO. HIDE_FRIEND_COMPOUNDS = NO -# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any -# documentation blocks found inside the body of a function. -# If set to NO (the default) these blocks will be appended to the -# function's detailed documentation block. +# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any +# documentation blocks found inside the body of a function. If set to NO, these +# blocks will be appended to the function's detailed documentation block. +# The default value is: NO. HIDE_IN_BODY_DOCS = NO -# The INTERNAL_DOCS tag determines if documentation -# that is typed after a \internal command is included. If the tag is set -# to NO (the default) then the documentation will be excluded. -# Set it to YES to include the internal documentation. +# The INTERNAL_DOCS tag determines if documentation that is typed after a +# \internal command is included. If the tag is set to NO then the documentation +# will be excluded. Set it to YES to include the internal documentation. +# The default value is: NO. INTERNAL_DOCS = NO -# If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate -# file names in lower-case letters. If set to YES upper-case letters are also +# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file +# names in lower-case letters. If set to YES, upper-case letters are also # allowed. This is useful if you have classes or files whose names only differ # in case and if your file system supports case sensitive file names. Windows # and Mac users are advised to set this option to NO. +# The default value is: system dependent. CASE_SENSE_NAMES = NO -# If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen -# will show members with their full class and namespace scopes in the -# documentation. If set to YES the scope will be hidden. +# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with +# their full class and namespace scopes in the documentation. If set to YES, the +# scope will be hidden. +# The default value is: NO. HIDE_SCOPE_NAMES = NO -# If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen -# will put a list of the files that are included by a file in the documentation -# of that file. +# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will +# append additional text to a page's title, such as Class Reference. If set to +# YES the compound reference will be hidden. +# The default value is: NO. + +HIDE_COMPOUND_REFERENCE= NO + +# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of +# the files that are included by a file in the documentation of that file. +# The default value is: YES. SHOW_INCLUDE_FILES = YES -# If the FORCE_LOCAL_INCLUDES tag is set to YES then Doxygen -# will list include files with double quotes in the documentation -# rather than with sharp brackets. +# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each +# grouped member an include statement to the documentation, telling the reader +# which file to include in order to use the member. +# The default value is: NO. + +SHOW_GROUPED_MEMB_INC = NO + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include +# files with double quotes in the documentation rather than with sharp brackets. +# The default value is: NO. FORCE_LOCAL_INCLUDES = NO -# If the INLINE_INFO tag is set to YES (the default) then a tag [inline] -# is inserted in the documentation for inline members. +# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the +# documentation for inline members. +# The default value is: YES. INLINE_INFO = YES -# If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen -# will sort the (detailed) documentation of file and class members -# alphabetically by member name. If set to NO the members will appear in -# declaration order. +# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the +# (detailed) documentation of file and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. +# The default value is: YES. SORT_MEMBER_DOCS = YES -# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the -# brief documentation of file, namespace and class members alphabetically -# by member name. If set to NO (the default) the members will appear in -# declaration order. +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief +# descriptions of file, namespace and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. Note that +# this will also influence the order of the classes in the class list. +# The default value is: NO. SORT_BRIEF_DOCS = YES -# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen -# will sort the (brief and detailed) documentation of class members so that -# constructors and destructors are listed first. If set to NO (the default) -# the constructors will appear in the respective orders defined by -# SORT_MEMBER_DOCS and SORT_BRIEF_DOCS. -# This tag will be ignored for brief docs if SORT_BRIEF_DOCS is set to NO -# and ignored for detailed docs if SORT_MEMBER_DOCS is set to NO. +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the +# (brief and detailed) documentation of class members so that constructors and +# destructors are listed first. If set to NO the constructors will appear in the +# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. +# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief +# member documentation. +# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting +# detailed member documentation. +# The default value is: NO. SORT_MEMBERS_CTORS_1ST = YES -# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the -# hierarchy of group names into alphabetical order. If set to NO (the default) -# the group names will appear in their defined order. +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy +# of group names into alphabetical order. If set to NO the group names will +# appear in their defined order. +# The default value is: NO. SORT_GROUP_NAMES = YES -# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be -# sorted by fully-qualified names, including namespaces. If set to -# NO (the default), the class list will be sorted only by class name, -# not including the namespace part. +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by +# fully-qualified names, including namespaces. If set to NO, the class list will +# be sorted only by class name, not including the namespace part. # Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. -# Note: This option applies only to the class list, not to the -# alphabetical list. +# Note: This option applies only to the class list, not to the alphabetical +# list. +# The default value is: NO. SORT_BY_SCOPE_NAME = NO -# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to -# do proper type resolution of all parameters of a function it will reject a -# match between the prototype and the implementation of a member function even -# if there is only one candidate or it is obvious which candidate to choose -# by doing a simple string match. By disabling STRICT_PROTO_MATCHING doxygen -# will still accept a match between prototype and implementation in such cases. +# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper +# type resolution of all parameters of a function it will reject a match between +# the prototype and the implementation of a member function even if there is +# only one candidate or it is obvious which candidate to choose by doing a +# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still +# accept a match between prototype and implementation in such cases. +# The default value is: NO. STRICT_PROTO_MATCHING = NO -# The GENERATE_TODOLIST tag can be used to enable (YES) or -# disable (NO) the todo list. This list is created by putting \todo -# commands in the documentation. +# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo +# list. This list is created by putting \todo commands in the documentation. +# The default value is: YES. GENERATE_TODOLIST = YES -# The GENERATE_TESTLIST tag can be used to enable (YES) or -# disable (NO) the test list. This list is created by putting \test -# commands in the documentation. +# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test +# list. This list is created by putting \test commands in the documentation. +# The default value is: YES. GENERATE_TESTLIST = YES -# The GENERATE_BUGLIST tag can be used to enable (YES) or -# disable (NO) the bug list. This list is created by putting \bug -# commands in the documentation. +# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug +# list. This list is created by putting \bug commands in the documentation. +# The default value is: YES. GENERATE_BUGLIST = YES -# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or -# disable (NO) the deprecated list. This list is created by putting -# \deprecated commands in the documentation. +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) +# the deprecated list. This list is created by putting \deprecated commands in +# the documentation. +# The default value is: YES. GENERATE_DEPRECATEDLIST= YES -# The ENABLED_SECTIONS tag can be used to enable conditional -# documentation sections, marked by \if sectionname ... \endif. +# The ENABLED_SECTIONS tag can be used to enable conditional documentation +# sections, marked by \if ... \endif and \cond +# ... \endcond blocks. ENABLED_SECTIONS = -# The MAX_INITIALIZER_LINES tag determines the maximum number of lines -# the initial value of a variable or macro consists of for it to appear in -# the documentation. If the initializer consists of more lines than specified -# here it will be hidden. Use a value of 0 to hide initializers completely. -# The appearance of the initializer of individual variables and macros in the -# documentation can be controlled using \showinitializer or \hideinitializer -# command in the documentation regardless of this setting. +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the +# initial value of a variable or macro / define can have for it to appear in the +# documentation. If the initializer consists of more lines than specified here +# it will be hidden. Use a value of 0 to hide initializers completely. The +# appearance of the value of individual variables and macros / defines can be +# controlled using \showinitializer or \hideinitializer command in the +# documentation regardless of this setting. +# Minimum value: 0, maximum value: 10000, default value: 30. MAX_INITIALIZER_LINES = 30 -# Set the SHOW_USED_FILES tag to NO to disable the list of files generated -# at the bottom of the documentation of classes and structs. If set to YES the +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at +# the bottom of the documentation of classes and structs. If set to YES, the # list will mention the files that were used to generate the documentation. +# The default value is: YES. SHOW_USED_FILES = YES -# Set the SHOW_FILES tag to NO to disable the generation of the Files page. -# This will remove the Files entry from the Quick Index and from the -# Folder Tree View (if specified). The default is YES. +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This +# will remove the Files entry from the Quick Index and from the Folder Tree View +# (if specified). +# The default value is: YES. SHOW_FILES = YES -# Set the SHOW_NAMESPACES tag to NO to disable the generation of the -# Namespaces page. -# This will remove the Namespaces entry from the Quick Index -# and from the Folder Tree View (if specified). The default is YES. +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces +# page. This will remove the Namespaces entry from the Quick Index and from the +# Folder Tree View (if specified). +# The default value is: YES. SHOW_NAMESPACES = YES # The FILE_VERSION_FILTER tag can be used to specify a program or script that # doxygen should invoke to get the current version for each file (typically from # the version control system). Doxygen will invoke the program by executing (via -# popen()) the command , where is the value of -# the FILE_VERSION_FILTER tag, and is the name of an input file -# provided by doxygen. Whatever the program writes to standard output -# is used as the file version. See the manual for examples. +# popen()) the command command input-file, where command is the value of the +# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided +# by doxygen. Whatever the program writes to standard output is used as the file +# version. For an example see the documentation. FILE_VERSION_FILTER = # The LAYOUT_FILE tag can be used to specify a layout file which will be parsed # by doxygen. The layout file controls the global structure of the generated # output files in an output format independent way. To create the layout file -# that represents doxygen's defaults, run doxygen with the -l option. -# You can optionally specify a file name after the option, if omitted -# DoxygenLayout.xml will be used as the name of the layout file. +# that represents doxygen's defaults, run doxygen with the -l option. You can +# optionally specify a file name after the option, if omitted DoxygenLayout.xml +# will be used as the name of the layout file. +# +# Note that if you run doxygen from a directory containing a file called +# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE +# tag is left empty. LAYOUT_FILE = -# The CITE_BIB_FILES tag can be used to specify one or more bib files -# containing the references data. This must be a list of .bib files. The -# .bib extension is automatically appended if omitted. Using this command -# requires the bibtex tool to be installed. See also -# http://en.wikipedia.org/wiki/BibTeX for more info. For LaTeX the style -# of the bibliography can be controlled using LATEX_BIB_STYLE. To use this -# feature you need bibtex and perl available in the search path. +# The CITE_BIB_FILES tag can be used to specify one or more bib files containing +# the reference definitions. This must be a list of .bib files. The .bib +# extension is automatically appended if omitted. This requires the bibtex tool +# to be installed. See also http://en.wikipedia.org/wiki/BibTeX for more info. +# For LaTeX the style of the bibliography can be controlled using +# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the +# search path. See also \cite for info how to create references. CITE_BIB_FILES = #--------------------------------------------------------------------------- -# configuration options related to warning and progress messages +# Configuration options related to warning and progress messages #--------------------------------------------------------------------------- -# The QUIET tag can be used to turn on/off the messages that are generated -# by doxygen. Possible values are YES and NO. If left blank NO is used. +# The QUIET tag can be used to turn on/off the messages that are generated to +# standard output by doxygen. If QUIET is set to YES this implies that the +# messages are off. +# The default value is: NO. QUIET = NO # The WARNINGS tag can be used to turn on/off the warning messages that are -# generated by doxygen. Possible values are YES and NO. If left blank -# NO is used. +# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES +# this implies that the warnings are on. +# +# Tip: Turn warnings on while writing the documentation. +# The default value is: YES. WARNINGS = YES -# If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings -# for undocumented members. If EXTRACT_ALL is set to YES then this flag will -# automatically be disabled. +# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate +# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: YES. WARN_IF_UNDOCUMENTED = YES -# If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for -# potential errors in the documentation, such as not documenting some -# parameters in a documented function, or documenting parameters that -# don't exist or using markup commands wrongly. +# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as not documenting some parameters +# in a documented function, or documenting parameters that don't exist or using +# markup commands wrongly. +# The default value is: YES. WARN_IF_DOC_ERROR = YES -# The WARN_NO_PARAMDOC option can be enabled to get warnings for -# functions that are documented, but have no documentation for their parameters -# or return value. If set to NO (the default) doxygen will only warn about -# wrong or incomplete parameter documentation, but not about the absence of -# documentation. +# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that +# are documented, but have no documentation for their parameters or return +# value. If set to NO, doxygen will only warn about wrong or incomplete +# parameter documentation, but not about the absence of documentation. +# The default value is: NO. WARN_NO_PARAMDOC = NO -# The WARN_FORMAT tag determines the format of the warning messages that -# doxygen can produce. The string should contain the $file, $line, and $text -# tags, which will be replaced by the file and line number from which the -# warning originated and the warning text. Optionally the format may contain -# $version, which will be replaced by the version of the file (if it could -# be obtained via FILE_VERSION_FILTER) +# The WARN_FORMAT tag determines the format of the warning messages that doxygen +# can produce. The string should contain the $file, $line, and $text tags, which +# will be replaced by the file and line number from which the warning originated +# and the warning text. Optionally the format may contain $version, which will +# be replaced by the version of the file (if it could be obtained via +# FILE_VERSION_FILTER) +# The default value is: $file:$line: $text. WARN_FORMAT = "$file:$line: $text " -# The WARN_LOGFILE tag can be used to specify a file to which warning -# and error messages should be written. If left blank the output is written -# to stderr. +# The WARN_LOGFILE tag can be used to specify a file to which warning and error +# messages should be written. If left blank the output is written to standard +# error (stderr). WARN_LOGFILE = #--------------------------------------------------------------------------- -# configuration options related to the input files +# Configuration options related to the input files #--------------------------------------------------------------------------- -# The INPUT tag can be used to specify the files and/or directories that contain -# documented source files. You may enter file names like "myfile.cpp" or -# directories like "/usr/src/myproject". Separate the files or directories -# with spaces. +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. +# Note: If this tag is empty the current directory is searched. INPUT = main.dox \ workflow.dox \ @@ -680,38 +777,40 @@ INPUT = main.dox \ ../../CoreLibs/src \ ../../KeywordSearch/src \ ../../RecentActivity/src \ -#../../ScalpelCarver/src \ ../../Testing/src \ ../../thunderbirdparser/src # This tag can be used to specify the character encoding of the source files -# that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is -# also the default input encoding. Doxygen uses libiconv (or the iconv built -# into libc) for the transcoding. See http://www.gnu.org/software/libiconv for -# the list of possible encodings. +# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses +# libiconv (or the iconv built into libc) for the transcoding. See the libiconv +# documentation (see: http://www.gnu.org/software/libiconv) for the list of +# possible encodings. +# The default value is: UTF-8. INPUT_ENCODING = UTF-8 # If the value of the INPUT tag contains directories, you can use the -# FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp -# and *.h) to filter out the source-files in the directories. If left -# blank the following patterns are tested: -# *.c *.cc *.cxx *.cpp *.c++ *.d *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh -# *.hxx *.hpp *.h++ *.idl *.odl *.cs *.php *.php3 *.inc *.m *.mm *.dox *.py -# *.f90 *.f *.for *.vhd *.vhdl +# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank the +# following patterns are tested:*.c, *.cc, *.cxx, *.cpp, *.c++, *.java, *.ii, +# *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, +# *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, +# *.md, *.mm, *.dox, *.py, *.f90, *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf, +# *.qsf, *.as and *.js. FILE_PATTERNS = *.java \ *.dox -# The RECURSIVE tag can be used to turn specify whether or not subdirectories -# should be searched for input files as well. Possible values are YES and NO. -# If left blank NO is used. +# The RECURSIVE tag can be used to specify whether or not subdirectories should +# be searched for input files as well. +# The default value is: NO. RECURSIVE = YES # The EXCLUDE tag can be used to specify files and/or directories that should be # excluded from the INPUT source files. This way you can easily exclude a # subdirectory from a directory tree whose root is specified with the INPUT tag. +# # Note that relative paths are relative to the directory from which doxygen is # run. @@ -720,14 +819,16 @@ EXCLUDE = # The EXCLUDE_SYMLINKS tag can be used to select whether or not files or # directories that are symbolic links (a Unix file system feature) are excluded # from the input. +# The default value is: NO. EXCLUDE_SYMLINKS = NO # If the value of the INPUT tag contains directories, you can use the # EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude -# certain files from those directories. Note that the wildcards are matched -# against the file with absolute path, so to exclude all test directories -# for example use the pattern */test/* +# certain files from those directories. +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories for example use the pattern */test/* EXCLUDE_PATTERNS = @@ -736,765 +837,1129 @@ EXCLUDE_PATTERNS = # output. The symbol name can be a fully qualified name, a word, or if the # wildcard * is used, a substring. Examples: ANamespace, AClass, # AClass::ANamespace, ANamespace::*Test +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories use the pattern */test/* EXCLUDE_SYMBOLS = -# The EXAMPLE_PATH tag can be used to specify one or more files or -# directories that contain example code fragments that are included (see -# the \include command). +# The EXAMPLE_PATH tag can be used to specify one or more files or directories +# that contain example code fragments that are included (see the \include +# command). EXAMPLE_PATH = # If the value of the EXAMPLE_PATH tag contains directories, you can use the -# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp -# and *.h) to filter out the source-files in the directories. If left -# blank all files are included. +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank all +# files are included. EXAMPLE_PATTERNS = # If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be -# searched for input files to be used with the \include or \dontinclude -# commands irrespective of the value of the RECURSIVE tag. -# Possible values are YES and NO. If left blank NO is used. +# searched for input files to be used with the \include or \dontinclude commands +# irrespective of the value of the RECURSIVE tag. +# The default value is: NO. EXAMPLE_RECURSIVE = NO -# The IMAGE_PATH tag can be used to specify one or more files or -# directories that contain image that are included in the documentation (see -# the \image command). +# The IMAGE_PATH tag can be used to specify one or more files or directories +# that contain images that are to be included in the documentation (see the +# \image command). IMAGE_PATH = . # The INPUT_FILTER tag can be used to specify a program that doxygen should # invoke to filter for each input file. Doxygen will invoke the filter program -# by executing (via popen()) the command , where -# is the value of the INPUT_FILTER tag, and is the name of an -# input file. Doxygen will then use the output that the filter program writes -# to standard output. -# If FILTER_PATTERNS is specified, this tag will be -# ignored. +# by executing (via popen()) the command: +# +# +# +# where is the value of the INPUT_FILTER tag, and is the +# name of an input file. Doxygen will then use the output that the filter +# program writes to standard output. If FILTER_PATTERNS is specified, this tag +# will be ignored. +# +# Note that the filter must not add or remove lines; it is applied before the +# code is scanned, but not when the output code is generated. If lines are added +# or removed, the anchors will not be placed correctly. INPUT_FILTER = # The FILTER_PATTERNS tag can be used to specify filters on a per file pattern -# basis. -# Doxygen will compare the file name with each pattern and apply the -# filter if there is a match. -# The filters are a list of the form: -# pattern=filter (like *.cpp=my_cpp_filter). See INPUT_FILTER for further -# info on how filters are used. If FILTER_PATTERNS is empty or if -# non of the patterns match the file name, INPUT_FILTER is applied. +# basis. Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. The filters are a list of the form: pattern=filter +# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how +# filters are used. If the FILTER_PATTERNS tag is empty or if none of the +# patterns match the file name, INPUT_FILTER is applied. FILTER_PATTERNS = # If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using -# INPUT_FILTER) will be used to filter the input files when producing source -# files to browse (i.e. when SOURCE_BROWSER is set to YES). +# INPUT_FILTER) will also be used to filter the input files that are used for +# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). +# The default value is: NO. FILTER_SOURCE_FILES = NO # The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file -# pattern. A pattern will override the setting for FILTER_PATTERN (if any) -# and it is also possible to disable source filtering for a specific pattern -# using *.ext= (so without naming a filter). This option only has effect when -# FILTER_SOURCE_FILES is enabled. +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and +# it is also possible to disable source filtering for a specific pattern using +# *.ext= (so without naming a filter). +# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. FILTER_SOURCE_PATTERNS = +# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want to reuse the introduction page also for the doxygen output. + +USE_MDFILE_AS_MAINPAGE = + #--------------------------------------------------------------------------- -# configuration options related to source browsing +# Configuration options related to source browsing #--------------------------------------------------------------------------- -# If the SOURCE_BROWSER tag is set to YES then a list of source files will -# be generated. Documented entities will be cross-referenced with these sources. -# Note: To get rid of all source code in the generated output, make sure also -# VERBATIM_HEADERS is set to NO. +# If the SOURCE_BROWSER tag is set to YES then a list of source files will be +# generated. Documented entities will be cross-referenced with these sources. +# +# Note: To get rid of all source code in the generated output, make sure that +# also VERBATIM_HEADERS is set to NO. +# The default value is: NO. SOURCE_BROWSER = YES -# Setting the INLINE_SOURCES tag to YES will include the body -# of functions and classes directly in the documentation. +# Setting the INLINE_SOURCES tag to YES will include the body of functions, +# classes and enums directly into the documentation. +# The default value is: NO. INLINE_SOURCES = NO -# Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct -# doxygen to hide any special comment blocks from generated source code -# fragments. Normal C, C++ and Fortran comments will always remain visible. +# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any +# special comment blocks from generated source code fragments. Normal C, C++ and +# Fortran comments will always remain visible. +# The default value is: YES. STRIP_CODE_COMMENTS = YES -# If the REFERENCED_BY_RELATION tag is set to YES -# then for each documented function all documented -# functions referencing it will be listed. +# If the REFERENCED_BY_RELATION tag is set to YES then for each documented +# function all documented functions referencing it will be listed. +# The default value is: NO. REFERENCED_BY_RELATION = YES -# If the REFERENCES_RELATION tag is set to YES -# then for each documented function all documented entities -# called/used by that function will be listed. +# If the REFERENCES_RELATION tag is set to YES then for each documented function +# all documented entities called/used by that function will be listed. +# The default value is: NO. REFERENCES_RELATION = YES -# If the REFERENCES_LINK_SOURCE tag is set to YES (the default) -# and SOURCE_BROWSER tag is set to YES, then the hyperlinks from -# functions in REFERENCES_RELATION and REFERENCED_BY_RELATION lists will -# link to the source code. -# Otherwise they will link to the documentation. +# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set +# to YES then the hyperlinks from functions in REFERENCES_RELATION and +# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will +# link to the documentation. +# The default value is: YES. REFERENCES_LINK_SOURCE = YES -# If the USE_HTAGS tag is set to YES then the references to source code -# will point to the HTML generated by the htags(1) tool instead of doxygen -# built-in source browser. The htags tool is part of GNU's global source -# tagging system (see http://www.gnu.org/software/global/global.html). You -# will need version 4.8.6 or higher. +# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the +# source code will show a tooltip with additional information such as prototype, +# brief description and links to the definition and documentation. Since this +# will make the HTML file larger and loading of large files a bit slower, you +# can opt to disable this feature. +# The default value is: YES. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +SOURCE_TOOLTIPS = YES + +# If the USE_HTAGS tag is set to YES then the references to source code will +# point to the HTML generated by the htags(1) tool instead of doxygen built-in +# source browser. The htags tool is part of GNU's global source tagging system +# (see http://www.gnu.org/software/global/global.html). You will need version +# 4.8.6 or higher. +# +# To use it do the following: +# - Install the latest version of global +# - Enable SOURCE_BROWSER and USE_HTAGS in the config file +# - Make sure the INPUT points to the root of the source tree +# - Run doxygen as normal +# +# Doxygen will invoke htags (and that will in turn invoke gtags), so these +# tools must be available from the command line (i.e. in the search path). +# +# The result: instead of the source browser generated by doxygen, the links to +# source code will now point to the output of htags. +# The default value is: NO. +# This tag requires that the tag SOURCE_BROWSER is set to YES. USE_HTAGS = NO -# If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen -# will generate a verbatim copy of the header file for each class for -# which an include is specified. Set to NO to disable this. +# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a +# verbatim copy of the header file for each class for which an include is +# specified. Set to NO to disable this. +# See also: Section \class. +# The default value is: YES. VERBATIM_HEADERS = YES +# If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the +# clang parser (see: http://clang.llvm.org/) for more accurate parsing at the +# cost of reduced performance. This can be particularly helpful with template +# rich C++ code for which doxygen's built-in parser lacks the necessary type +# information. +# Note: The availability of this option depends on whether or not doxygen was +# compiled with the --with-libclang option. +# The default value is: NO. + +CLANG_ASSISTED_PARSING = NO + +# If clang assisted parsing is enabled you can provide the compiler with command +# line options that you would normally use when invoking the compiler. Note that +# the include paths will already be set by doxygen for the files and directories +# specified with INPUT and INCLUDE_PATH. +# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. + +CLANG_OPTIONS = + #--------------------------------------------------------------------------- -# configuration options related to the alphabetical class index +# Configuration options related to the alphabetical class index #--------------------------------------------------------------------------- -# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index -# of all compounds will be generated. Enable this if the project -# contains a lot of classes, structs, unions or interfaces. +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all +# compounds will be generated. Enable this if the project contains a lot of +# classes, structs, unions or interfaces. +# The default value is: YES. ALPHABETICAL_INDEX = YES -# If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then -# the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns -# in which this list will be split (can be a number in the range [1..20]) +# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in +# which the alphabetical index list will be split. +# Minimum value: 1, maximum value: 20, default value: 5. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. COLS_IN_ALPHA_INDEX = 5 -# In case all classes in a project start with a common prefix, all -# classes will be put under the same header in the alphabetical index. -# The IGNORE_PREFIX tag can be used to specify one or more prefixes that -# should be ignored while generating the index headers. +# In case all classes in a project start with a common prefix, all classes will +# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag +# can be used to specify a prefix (or a list of prefixes) that should be ignored +# while generating the index headers. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. IGNORE_PREFIX = #--------------------------------------------------------------------------- -# configuration options related to the HTML output +# Configuration options related to the HTML output #--------------------------------------------------------------------------- -# If the GENERATE_HTML tag is set to YES (the default) Doxygen will -# generate HTML output. +# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output +# The default value is: YES. GENERATE_HTML = YES -# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. -# If a relative path is entered the value of OUTPUT_DIRECTORY will be -# put in front of it. If left blank `html' will be used as the default path. +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: html. +# This tag requires that the tag GENERATE_HTML is set to YES. -HTML_OUTPUT = api-docs/3.1 +HTML_OUTPUT = api-docs/3.1/ -# The HTML_FILE_EXTENSION tag can be used to specify the file extension for -# each generated HTML page (for example: .htm,.php,.asp). If it is left blank -# doxygen will generate files with .html extension. +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each +# generated HTML page (for example: .htm, .php, .asp). +# The default value is: .html. +# This tag requires that the tag GENERATE_HTML is set to YES. HTML_FILE_EXTENSION = .html -# The HTML_HEADER tag can be used to specify a personal HTML header for -# each generated HTML page. If it is left blank doxygen will generate a -# standard header. Note that when using a custom header you are responsible -# for the proper inclusion of any scripts and style sheets that doxygen -# needs, which is dependent on the configuration options used. -# It is advised to generate a default header using "doxygen -w html -# header.html footer.html stylesheet.css YourConfigFile" and then modify -# that header. Note that the header is subject to change so you typically -# have to redo this when upgrading to a newer version of doxygen or when -# changing the value of configuration settings such as GENERATE_TREEVIEW! +# The HTML_HEADER tag can be used to specify a user-defined HTML header file for +# each generated HTML page. If the tag is left blank doxygen will generate a +# standard header. +# +# To get valid HTML the header file that includes any scripts and style sheets +# that doxygen needs, which is dependent on the configuration options used (e.g. +# the setting GENERATE_TREEVIEW). It is highly recommended to start with a +# default header using +# doxygen -w html new_header.html new_footer.html new_stylesheet.css +# YourConfigFile +# and then modify the file new_header.html. See also section "Doxygen usage" +# for information on how to generate the default header that doxygen normally +# uses. +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of doxygen. For a description +# of the possible markers and block names see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. HTML_HEADER = -# The HTML_FOOTER tag can be used to specify a personal HTML footer for -# each generated HTML page. If it is left blank doxygen will generate a -# standard footer. +# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each +# generated HTML page. If the tag is left blank doxygen will generate a standard +# footer. See HTML_HEADER for more information on how to generate a default +# footer and what special commands can be used inside the footer. See also +# section "Doxygen usage" for information on how to generate the default footer +# that doxygen normally uses. +# This tag requires that the tag GENERATE_HTML is set to YES. HTML_FOOTER = footer.html -# The HTML_STYLESHEET tag can be used to specify a user-defined cascading -# style sheet that is used by each HTML page. It can be used to -# fine-tune the look of the HTML output. If left blank doxygen will -# generate a default style sheet. Note that it is recommended to use -# HTML_EXTRA_STYLESHEET instead of this one, as it is more robust and this -# tag will in the future become obsolete. +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style +# sheet that is used by each HTML page. It can be used to fine-tune the look of +# the HTML output. If left blank doxygen will generate a default style sheet. +# See also section "Doxygen usage" for information on how to generate the style +# sheet that doxygen normally uses. +# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as +# it is more robust and this tag (HTML_STYLESHEET) will in the future become +# obsolete. +# This tag requires that the tag GENERATE_HTML is set to YES. HTML_STYLESHEET = -# The HTML_EXTRA_STYLESHEET tag can be used to specify an additional -# user-defined cascading style sheet that is included after the standard -# style sheets created by doxygen. Using this option one can overrule -# certain style aspects. This is preferred over using HTML_STYLESHEET -# since it does not replace the standard style sheet and is therefor more -# robust against future updates. Doxygen will copy the style sheet file to -# the output directory. +# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined +# cascading style sheets that are included after the standard style sheets +# created by doxygen. Using this option one can overrule certain style aspects. +# This is preferred over using HTML_STYLESHEET since it does not replace the +# standard style sheet and is therefore more robust against future updates. +# Doxygen will copy the style sheet files to the output directory. +# Note: The order of the extra style sheet files is of importance (e.g. the last +# style sheet in the list overrules the setting of the previous ones in the +# list). For an example see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. HTML_EXTRA_STYLESHEET = # The HTML_EXTRA_FILES tag can be used to specify one or more extra images or # other source files which should be copied to the HTML output directory. Note # that these files will be copied to the base HTML output directory. Use the -# $relpath$ marker in the HTML_HEADER and/or HTML_FOOTER files to load these -# files. In the HTML_STYLESHEET file, use the file name only. Also note that -# the files will be copied as-is; there are no commands or markers available. +# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that the +# files will be copied as-is; there are no commands or markers available. +# This tag requires that the tag GENERATE_HTML is set to YES. HTML_EXTRA_FILES = -# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. -# Doxygen will adjust the colors in the style sheet and background images -# according to this color. Hue is specified as an angle on a colorwheel, -# see http://en.wikipedia.org/wiki/Hue for more information. -# For instance the value 0 represents red, 60 is yellow, 120 is green, -# 180 is cyan, 240 is blue, 300 purple, and 360 is red again. -# The allowed range is 0 to 359. +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen +# will adjust the colors in the style sheet and background images according to +# this color. Hue is specified as an angle on a colorwheel, see +# http://en.wikipedia.org/wiki/Hue for more information. For instance the value +# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 +# purple, and 360 is red again. +# Minimum value: 0, maximum value: 359, default value: 220. +# This tag requires that the tag GENERATE_HTML is set to YES. HTML_COLORSTYLE_HUE = 220 -# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of -# the colors in the HTML output. For a value of 0 the output will use -# grayscales only. A value of 255 will produce the most vivid colors. +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors +# in the HTML output. For a value of 0 the output will use grayscales only. A +# value of 255 will produce the most vivid colors. +# Minimum value: 0, maximum value: 255, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. HTML_COLORSTYLE_SAT = 100 -# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to -# the luminance component of the colors in the HTML output. Values below -# 100 gradually make the output lighter, whereas values above 100 make -# the output darker. The value divided by 100 is the actual gamma applied, -# so 80 represents a gamma of 0.8, The value 220 represents a gamma of 2.2, -# and 100 does not change the gamma. +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the +# luminance component of the colors in the HTML output. Values below 100 +# gradually make the output lighter, whereas values above 100 make the output +# darker. The value divided by 100 is the actual gamma applied, so 80 represents +# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not +# change the gamma. +# Minimum value: 40, maximum value: 240, default value: 80. +# This tag requires that the tag GENERATE_HTML is set to YES. HTML_COLORSTYLE_GAMMA = 80 # If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML -# page will contain the date and time when the page was generated. Setting -# this to NO can help when comparing the output of multiple runs. +# page will contain the date and time when the page was generated. Setting this +# to NO can help when comparing the output of multiple runs. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. HTML_TIMESTAMP = YES # If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML # documentation will contain sections that can be hidden and shown after the # page has loaded. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. HTML_DYNAMIC_SECTIONS = YES -# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of -# entries shown in the various tree structured indices initially; the user -# can expand and collapse entries dynamically later on. Doxygen will expand -# the tree to such a level that at most the specified number of entries are -# visible (unless a fully collapsed tree already exceeds this amount). -# So setting the number of entries 1 will produce a full collapsed tree by -# default. 0 is a special value representing an infinite number of entries -# and will result in a full expanded tree by default. +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries +# shown in the various tree structured indices initially; the user can expand +# and collapse entries dynamically later on. Doxygen will expand the tree to +# such a level that at most the specified number of entries are visible (unless +# a fully collapsed tree already exceeds this amount). So setting the number of +# entries 1 will produce a full collapsed tree by default. 0 is a special value +# representing an infinite number of entries and will result in a full expanded +# tree by default. +# Minimum value: 0, maximum value: 9999, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. HTML_INDEX_NUM_ENTRIES = 100 -# If the GENERATE_DOCSET tag is set to YES, additional index files -# will be generated that can be used as input for Apple's Xcode 3 -# integrated development environment, introduced with OSX 10.5 (Leopard). -# To create a documentation set, doxygen will generate a Makefile in the -# HTML output directory. Running make will produce the docset in that -# directory and running "make install" will install the docset in -# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find -# it at startup. -# See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html +# If the GENERATE_DOCSET tag is set to YES, additional index files will be +# generated that can be used as input for Apple's Xcode 3 integrated development +# environment (see: http://developer.apple.com/tools/xcode/), introduced with +# OSX 10.5 (Leopard). To create a documentation set, doxygen will generate a +# Makefile in the HTML output directory. Running make will produce the docset in +# that directory and running make install will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at +# startup. See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html # for more information. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. GENERATE_DOCSET = YES -# When GENERATE_DOCSET tag is set to YES, this tag determines the name of the -# feed. A documentation feed provides an umbrella under which multiple -# documentation sets from a single provider (such as a company or product suite) -# can be grouped. +# This tag determines the name of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# The default value is: Doxygen generated docs. +# This tag requires that the tag GENERATE_DOCSET is set to YES. DOCSET_FEEDNAME = "Doxygen docs" -# When GENERATE_DOCSET tag is set to YES, this tag specifies a string that -# should uniquely identify the documentation set bundle. This should be a -# reverse domain-name style string, e.g. com.mycompany.MyDocSet. Doxygen -# will append .docset to the name. +# This tag specifies a string that should uniquely identify the documentation +# set bundle. This should be a reverse domain-name style string, e.g. +# com.mycompany.MyDocSet. Doxygen will append .docset to the name. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_DOCSET is set to YES. DOCSET_BUNDLE_ID = org.doxygen.Doxygen -# When GENERATE_PUBLISHER_ID tag specifies a string that should uniquely -# identify the documentation publisher. This should be a reverse domain-name -# style string, e.g. com.mycompany.MyDocSet.documentation. +# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify +# the documentation publisher. This should be a reverse domain-name style +# string, e.g. com.mycompany.MyDocSet.documentation. +# The default value is: org.doxygen.Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. DOCSET_PUBLISHER_ID = org.doxygen.Publisher -# The GENERATE_PUBLISHER_NAME tag identifies the documentation publisher. +# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. +# The default value is: Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. DOCSET_PUBLISHER_NAME = Publisher -# If the GENERATE_HTMLHELP tag is set to YES, additional index files -# will be generated that can be used as input for tools like the -# Microsoft HTML help workshop to generate a compiled HTML help file (.chm) -# of the generated HTML documentation. +# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three +# additional HTML index files: index.hhp, index.hhc, and index.hhk. The +# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop +# (see: http://www.microsoft.com/en-us/download/details.aspx?id=21138) on +# Windows. +# +# The HTML Help Workshop contains a compiler that can convert all HTML output +# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML +# files are now used as the Windows 98 help format, and will replace the old +# Windows help format (.hlp) on all Windows platforms in the future. Compressed +# HTML files also contain an index, a table of contents, and you can search for +# words in the documentation. The HTML workshop also contains a viewer for +# compressed HTML files. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. GENERATE_HTMLHELP = YES -# If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can -# be used to specify the file name of the resulting .chm file. You -# can add a path in front of the file if the result should not be +# The CHM_FILE tag can be used to specify the file name of the resulting .chm +# file. You can add a path in front of the file if the result should not be # written to the html output directory. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. CHM_FILE = -# If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can -# be used to specify the location (absolute path including file name) of -# the HTML help compiler (hhc.exe). If non-empty doxygen will try to run -# the HTML help compiler on the generated index.hhp. +# The HHC_LOCATION tag can be used to specify the location (absolute path +# including file name) of the HTML help compiler (hhc.exe). If non-empty, +# doxygen will try to run the HTML help compiler on the generated index.hhp. +# The file has to be specified with full path. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. HHC_LOCATION = -# If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag -# controls if a separate .chi index file is generated (YES) or that -# it should be included in the master .chm file (NO). +# The GENERATE_CHI flag controls if a separate .chi index file is generated +# (YES) or that it should be included in the master .chm file (NO). +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. GENERATE_CHI = NO -# If the GENERATE_HTMLHELP tag is set to YES, the CHM_INDEX_ENCODING -# is used to encode HtmlHelp index (hhk), content (hhc) and project file -# content. +# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) +# and project file content. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. CHM_INDEX_ENCODING = -# If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag -# controls whether a binary table of contents is generated (YES) or a -# normal table of contents (NO) in the .chm file. +# The BINARY_TOC flag controls whether a binary table of contents is generated +# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it +# enables the Previous and Next buttons. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. BINARY_TOC = NO -# The TOC_EXPAND flag can be set to YES to add extra items for group members -# to the contents of the HTML help documentation and to the tree view. +# The TOC_EXPAND flag can be set to YES to add extra items for group members to +# the table of contents of the HTML help documentation and to the tree view. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. TOC_EXPAND = NO # If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and -# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated -# that can be used as input for Qt's qhelpgenerator to generate a -# Qt Compressed Help (.qch) of the generated HTML documentation. +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that +# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help +# (.qch) of the generated HTML documentation. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. GENERATE_QHP = NO -# If the QHG_LOCATION tag is specified, the QCH_FILE tag can -# be used to specify the file name of the resulting .qch file. -# The path specified is relative to the HTML output folder. +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify +# the file name of the resulting .qch file. The path specified is relative to +# the HTML output folder. +# This tag requires that the tag GENERATE_QHP is set to YES. QCH_FILE = -# The QHP_NAMESPACE tag specifies the namespace to use when generating -# Qt Help Project output. For more information please see -# http://doc.trolltech.com/qthelpproject.html#namespace +# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help +# Project output. For more information please see Qt Help Project / Namespace +# (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#namespace). +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_QHP is set to YES. QHP_NAMESPACE = org.doxygen.Project -# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating -# Qt Help Project output. For more information please see -# http://doc.trolltech.com/qthelpproject.html#virtual-folders +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt +# Help Project output. For more information please see Qt Help Project / Virtual +# Folders (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#virtual- +# folders). +# The default value is: doc. +# This tag requires that the tag GENERATE_QHP is set to YES. QHP_VIRTUAL_FOLDER = doc -# If QHP_CUST_FILTER_NAME is set, it specifies the name of a custom filter to -# add. For more information please see -# http://doc.trolltech.com/qthelpproject.html#custom-filters +# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom +# filter to add. For more information please see Qt Help Project / Custom +# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. QHP_CUST_FILTER_NAME = -# The QHP_CUST_FILT_ATTRS tag specifies the list of the attributes of the -# custom filter to add. For more information please see -# -# Qt Help Project / Custom Filters. +# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see Qt Help Project / Custom +# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. QHP_CUST_FILTER_ATTRS = # The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this -# project's -# filter section matches. -# -# Qt Help Project / Filter Attributes. +# project's filter section matches. Qt Help Project / Filter Attributes (see: +# http://qt-project.org/doc/qt-4.8/qthelpproject.html#filter-attributes). +# This tag requires that the tag GENERATE_QHP is set to YES. QHP_SECT_FILTER_ATTRS = -# If the GENERATE_QHP tag is set to YES, the QHG_LOCATION tag can -# be used to specify the location of Qt's qhelpgenerator. -# If non-empty doxygen will try to run qhelpgenerator on the generated -# .qhp file. +# The QHG_LOCATION tag can be used to specify the location of Qt's +# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the +# generated .qhp file. +# This tag requires that the tag GENERATE_QHP is set to YES. QHG_LOCATION = -# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files -# will be generated, which together with the HTML files, form an Eclipse help -# plugin. To install this plugin and make it available under the help contents -# menu in Eclipse, the contents of the directory containing the HTML and XML -# files needs to be copied into the plugins directory of eclipse. The name of -# the directory within the plugins directory should be the same as -# the ECLIPSE_DOC_ID value. After copying Eclipse needs to be restarted before -# the help appears. +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be +# generated, together with the HTML files, they form an Eclipse help plugin. To +# install this plugin and make it available under the help contents menu in +# Eclipse, the contents of the directory containing the HTML and XML files needs +# to be copied into the plugins directory of eclipse. The name of the directory +# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. +# After copying Eclipse needs to be restarted before the help appears. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. GENERATE_ECLIPSEHELP = NO -# A unique identifier for the eclipse help plugin. When installing the plugin -# the directory name containing the HTML and XML files should also have -# this name. +# A unique identifier for the Eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have this +# name. Each documentation set should have its own identifier. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. ECLIPSE_DOC_ID = org.doxygen.Project -# The DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) -# at top of each HTML page. The value NO (the default) enables the index and -# the value YES disables it. Since the tabs have the same information as the -# navigation tree you can set this option to NO if you already set -# GENERATE_TREEVIEW to YES. +# If you want full control over the layout of the generated HTML pages it might +# be necessary to disable the index and replace it with your own. The +# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top +# of each HTML page. A value of NO enables the index and the value YES disables +# it. Since the tabs in the index contain the same information as the navigation +# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. DISABLE_INDEX = NO # The GENERATE_TREEVIEW tag is used to specify whether a tree-like index -# structure should be generated to display hierarchical information. -# If the tag value is set to YES, a side panel will be generated -# containing a tree-like index structure (just like the one that -# is generated for HTML Help). For this to work a browser that supports -# JavaScript, DHTML, CSS and frames is required (i.e. any modern browser). -# Windows users are probably better off using the HTML help feature. -# Since the tree basically has the same information as the tab index you -# could consider to set DISABLE_INDEX to NO when enabling this option. +# structure should be generated to display hierarchical information. If the tag +# value is set to YES, a side panel will be generated containing a tree-like +# index structure (just like the one that is generated for HTML Help). For this +# to work a browser that supports JavaScript, DHTML, CSS and frames is required +# (i.e. any modern browser). Windows users are probably better off using the +# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can +# further fine-tune the look of the index. As an example, the default style +# sheet generated by doxygen has an example that shows how to put an image at +# the root of the tree instead of the PROJECT_NAME. Since the tree basically has +# the same information as the tab index, you could consider setting +# DISABLE_INDEX to YES when enabling this option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. -GENERATE_TREEVIEW = YES +GENERATE_TREEVIEW = NO -# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values -# (range [0,1..20]) that doxygen will group on one line in the generated HTML -# documentation. Note that a value of 0 will completely suppress the enum -# values from appearing in the overview section. +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that +# doxygen will group on one line in the generated HTML documentation. +# +# Note that a value of 0 will completely suppress the enum values from appearing +# in the overview section. +# Minimum value: 0, maximum value: 20, default value: 4. +# This tag requires that the tag GENERATE_HTML is set to YES. ENUM_VALUES_PER_LINE = 4 -# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be -# used to set the initial width (in pixels) of the frame in which the tree -# is shown. +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used +# to set the initial width (in pixels) of the frame in which the tree is shown. +# Minimum value: 0, maximum value: 1500, default value: 250. +# This tag requires that the tag GENERATE_HTML is set to YES. TREEVIEW_WIDTH = 250 -# When the EXT_LINKS_IN_WINDOW option is set to YES doxygen will open -# links to external symbols imported via tag files in a separate window. +# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to +# external symbols imported via tag files in a separate window. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. EXT_LINKS_IN_WINDOW = YES -# Use this tag to change the font size of Latex formulas included -# as images in the HTML documentation. The default is 10. Note that -# when you change the font size after a successful doxygen run you need -# to manually remove any form_*.png images from the HTML output directory -# to force them to be regenerated. +# Use this tag to change the font size of LaTeX formulas included as images in +# the HTML documentation. When you change the font size after a successful +# doxygen run you need to manually remove any form_*.png images from the HTML +# output directory to force them to be regenerated. +# Minimum value: 8, maximum value: 50, default value: 10. +# This tag requires that the tag GENERATE_HTML is set to YES. FORMULA_FONTSIZE = 10 # Use the FORMULA_TRANPARENT tag to determine whether or not the images -# generated for formulas are transparent PNGs. Transparent PNGs are -# not supported properly for IE 6.0, but are supported on all modern browsers. -# Note that when changing this option you need to delete any form_*.png files -# in the HTML output before the changes have effect. +# generated for formulas are transparent PNGs. Transparent PNGs are not +# supported properly for IE 6.0, but are supported on all modern browsers. +# +# Note that when changing this option you need to delete any form_*.png files in +# the HTML output directory before the changes have effect. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. FORMULA_TRANSPARENT = YES -# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax -# (see http://www.mathjax.org) which uses client side Javascript for the -# rendering instead of using prerendered bitmaps. Use this if you do not -# have LaTeX installed or if you want to formulas look prettier in the HTML -# output. When enabled you may also need to install MathJax separately and -# configure the path to it using the MATHJAX_RELPATH option. +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see +# http://www.mathjax.org) which uses client side Javascript for the rendering +# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX +# installed or if you want to formulas look prettier in the HTML output. When +# enabled you may also need to install MathJax separately and configure the path +# to it using the MATHJAX_RELPATH option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. USE_MATHJAX = NO -# When MathJax is enabled you need to specify the location relative to the -# HTML output directory using the MATHJAX_RELPATH option. The destination -# directory should contain the MathJax.js script. For instance, if the mathjax -# directory is located at the same level as the HTML output directory, then -# MATHJAX_RELPATH should be ../mathjax. The default value points to -# the MathJax Content Delivery Network so you can quickly see the result without -# installing MathJax. -# However, it is strongly recommended to install a local -# copy of MathJax from http://www.mathjax.org before deployment. +# When MathJax is enabled you can set the default output format to be used for +# the MathJax output. See the MathJax site (see: +# http://docs.mathjax.org/en/latest/output.html) for more details. +# Possible values are: HTML-CSS (which is slower, but has the best +# compatibility), NativeMML (i.e. MathML) and SVG. +# The default value is: HTML-CSS. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_FORMAT = HTML-CSS + +# When MathJax is enabled you need to specify the location relative to the HTML +# output directory using the MATHJAX_RELPATH option. The destination directory +# should contain the MathJax.js script. For instance, if the mathjax directory +# is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax +# Content Delivery Network so you can quickly see the result without installing +# MathJax. However, it is strongly recommended to install a local copy of +# MathJax from http://www.mathjax.org before deployment. +# The default value is: http://cdn.mathjax.org/mathjax/latest. +# This tag requires that the tag USE_MATHJAX is set to YES. MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest -# The MATHJAX_EXTENSIONS tag can be used to specify one or MathJax extension -# names that should be enabled during MathJax rendering. +# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax +# extension names that should be enabled during MathJax rendering. For example +# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# This tag requires that the tag USE_MATHJAX is set to YES. MATHJAX_EXTENSIONS = -# When the SEARCHENGINE tag is enabled doxygen will generate a search box -# for the HTML output. The underlying search engine uses javascript -# and DHTML and should work on any modern browser. Note that when using -# HTML help (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets -# (GENERATE_DOCSET) there is already a search function so this one should -# typically be disabled. For large projects the javascript based search engine -# can be slow, then enabling SERVER_BASED_SEARCH may provide a better solution. +# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces +# of code that will be used on startup of the MathJax code. See the MathJax site +# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an +# example see the documentation. +# This tag requires that the tag USE_MATHJAX is set to YES. -SEARCHENGINE = YES +MATHJAX_CODEFILE = + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box for +# the HTML output. The underlying search engine uses javascript and DHTML and +# should work on any modern browser. Note that when using HTML help +# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) +# there is already a search function so this one should typically be disabled. +# For large projects the javascript based search engine can be slow, then +# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to +# search using the keyboard; to jump to the search box use + S +# (what the is depends on the OS and browser, but it is typically +# , /