diff --git a/ImageAnalyzer/.gitattributes b/ImageAnalyzer/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..ea6aa14ebc2b6e62c55617c5a681521808e75d3c --- /dev/null +++ b/ImageAnalyzer/.gitattributes @@ -0,0 +1,13 @@ +*.java text diff=java + +*.txt text +*.sh text +*.mf text +*.xml text +*.form text +*.properties text +*.html text diff=html +*.dox text +Doxyfile text + +*.py text diff=python diff --git a/ImageAnalyzer/.gitignore b/ImageAnalyzer/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..9baf66d36165ea3285adcd4b3e8d7692c614c58d --- /dev/null +++ b/ImageAnalyzer/.gitignore @@ -0,0 +1,44 @@ + +#ignore thumbnails created by windows +Thumbs.db +#Ignore files build by Visual Studio +*.obj +*.exe +*.pdb +*.user +*.aps +*.pch +*.vspscc +*_i.c +*_p.c +*.ncb +*.suo +*.tlb +*.tlh +*.bak +*.cache +*.ilk +*.log +[Bb]in +[Dd]ebug*/ +*.lib +*.sbr +obj/ + +_ReSharper*/ +[Tt]est[Rr]esult* + +#Ignore Java class files +*.class +build/* + +*/javadoc/* + +nbproject/private/* + +*.hprof +*.nps + + + + diff --git a/ImageAnalyzer/LICENSE-2.0.txt b/ImageAnalyzer/LICENSE-2.0.txt new file mode 100644 index 0000000000000000000000000000000000000000..d645695673349e3947e8e5ae42332d0ac3164cd7 --- /dev/null +++ b/ImageAnalyzer/LICENSE-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/ImageAnalyzer/build.xml b/ImageAnalyzer/build.xml new file mode 100644 index 0000000000000000000000000000000000000000..4cbe0d1139dc67398397732492952b226228b632 --- /dev/null +++ b/ImageAnalyzer/build.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- You may freely edit this file. See harness/README in the NetBeans platform --> +<!-- for some information on what you could do (e.g. targets to override). --> +<!-- If you delete this file and reopen the project it will be recreated. --> +<project name="com.basistech.imageanalysis" default="netbeans" basedir="."> + <description>Builds, tests, and runs the project com.basistech.imageanalysis.</description> + <import file="nbproject/build-impl.xml"/> +</project> diff --git a/ImageAnalyzer/manifest.mf b/ImageAnalyzer/manifest.mf new file mode 100644 index 0000000000000000000000000000000000000000..537c9cf8b0c579a62a14b5dd04cfac05de0096c0 --- /dev/null +++ b/ImageAnalyzer/manifest.mf @@ -0,0 +1,5 @@ +Manifest-Version: 1.0 +OpenIDE-Module: org.sleuthkit.autopsy.imageanalyzer +OpenIDE-Module-Localizing-Bundle: org/sleuthkit/autopsy/imageanalyzer/Bundle.properties +OpenIDE-Module-Specification-Version: 1.0 + diff --git a/ImageAnalyzer/nbproject/build-impl.xml b/ImageAnalyzer/nbproject/build-impl.xml new file mode 100644 index 0000000000000000000000000000000000000000..4cfa3e50100a2da8fd5d74c482882dd8d8f86c13 --- /dev/null +++ b/ImageAnalyzer/nbproject/build-impl.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +*** GENERATED FROM project.xml - DO NOT EDIT *** +*** EDIT ../build.xml INSTEAD *** +--> +<project name="com.basistech.imageanalysis-impl" basedir=".."> + <fail message="Please build using Ant 1.7.1 or higher."> + <condition> + <not> + <antversion atleast="1.7.1"/> + </not> + </condition> + </fail> + <property file="nbproject/private/platform-private.properties"/> + <property file="nbproject/platform.properties"/> + <macrodef name="property" uri="http://www.netbeans.org/ns/nb-module-project/2"> + <attribute name="name"/> + <attribute name="value"/> + <sequential> + <property name="@{name}" value="${@{value}}"/> + </sequential> + </macrodef> + <macrodef name="evalprops" uri="http://www.netbeans.org/ns/nb-module-project/2"> + <attribute name="property"/> + <attribute name="value"/> + <sequential> + <property name="@{property}" value="@{value}"/> + </sequential> + </macrodef> + <property file="${user.properties.file}"/> + <nbmproject2:property name="harness.dir" value="nbplatform.${nbplatform.active}.harness.dir" xmlns:nbmproject2="http://www.netbeans.org/ns/nb-module-project/2"/> + <nbmproject2:property name="nbplatform.active.dir" value="nbplatform.${nbplatform.active}.netbeans.dest.dir" xmlns:nbmproject2="http://www.netbeans.org/ns/nb-module-project/2"/> + <nbmproject2:evalprops property="cluster.path.evaluated" value="${cluster.path}" xmlns:nbmproject2="http://www.netbeans.org/ns/nb-module-project/2"/> + <fail message="Path to 'platform' cluster missing in $${cluster.path} property or using corrupt Netbeans Platform (missing harness)."> + <condition> + <not> + <contains string="${cluster.path.evaluated}" substring="platform"/> + </not> + </condition> + </fail> + <import file="${harness.dir}/build.xml"/> +</project> diff --git a/ImageAnalyzer/nbproject/genfiles.properties b/ImageAnalyzer/nbproject/genfiles.properties new file mode 100644 index 0000000000000000000000000000000000000000..96475b54027488db631912d1bc90e97456b1f298 --- /dev/null +++ b/ImageAnalyzer/nbproject/genfiles.properties @@ -0,0 +1,8 @@ +build.xml.data.CRC32=673987f7 +build.xml.script.CRC32=eee7b5a4 +build.xml.stylesheet.CRC32=a56c6a5b@2.56.1 +# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml. +# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you. +nbproject/build-impl.xml.data.CRC32=673987f7 +nbproject/build-impl.xml.script.CRC32=92d95aee +nbproject/build-impl.xml.stylesheet.CRC32=238281d1@2.56.1 diff --git a/ImageAnalyzer/nbproject/platform.properties b/ImageAnalyzer/nbproject/platform.properties new file mode 100644 index 0000000000000000000000000000000000000000..dc5c66275b4a0c1c6a3996f1c88f19733042e27a --- /dev/null +++ b/ImageAnalyzer/nbproject/platform.properties @@ -0,0 +1,7 @@ +cluster.path=\ + ${nbplatform.active.dir}/autopsy:\ + ${nbplatform.active.dir}/harness:\ + ${nbplatform.active.dir}/java:\ + ${nbplatform.active.dir}/platform +nbjdk.active=JDK_1.8u20 +nbplatform.active=Autopsy_Dev diff --git a/ImageAnalyzer/nbproject/project.properties b/ImageAnalyzer/nbproject/project.properties new file mode 100644 index 0000000000000000000000000000000000000000..259ddd1e7dda6b0126cd1d1c9ab0825146a01e21 --- /dev/null +++ b/ImageAnalyzer/nbproject/project.properties @@ -0,0 +1,13 @@ +file.reference.commons-pool2-2.0-javadoc.jar=release/modules/ext/commons-pool2-2.0-javadoc.jar +file.reference.commons-pool2-2.0-sources.jar=release/modules/ext/commons-pool2-2.0-sources.jar +file.reference.commons-pool2-2.0.jar=release/modules/ext/commons-pool2-2.0.jar +file.reference.controlsfx-8.0.6.jar=release/modules/ext/controlsfx-8.0.6.jar +file.reference.jcip-annotations-src.jar=release/modules/ext/jcip-annotations-src.jar +file.reference.jcip-annotations.jar=release/modules/ext/jcip-annotations.jar +file.reference.sqlite-jdbc-3.7.8-SNAPSHOT.jar=release/modules/ext/sqlite-jdbc-3.7.8-SNAPSHOT.jar +javac.source=1.8 +javac.compilerargs=-Xlint -Xlint:-serial +license.file=LICENSE-2.0.txt +nbm.homepage=http://www.sleuthkit.org/ +nbm.needs.restart=true +project.license=eureka diff --git a/ImageAnalyzer/nbproject/project.xml b/ImageAnalyzer/nbproject/project.xml new file mode 100644 index 0000000000000000000000000000000000000000..29a4d98eaa4aec7a4bfb71946fb7dfcd753bbdd3 --- /dev/null +++ b/ImageAnalyzer/nbproject/project.xml @@ -0,0 +1,150 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://www.netbeans.org/ns/project/1"> + <type>org.netbeans.modules.apisupport.project</type> + <configuration> + <data xmlns="http://www.netbeans.org/ns/nb-module-project/3"> + <code-name-base>org.sleuthkit.autopsy.imageanalyzer</code-name-base> + <standalone/> + <module-dependencies> + <dependency> + <code-name-base>org.netbeans.api.progress</code-name-base> + <build-prerequisite/> + <compile-dependency/> + <run-dependency> + <release-version>1</release-version> + <specification-version>1.32.1</specification-version> + </run-dependency> + </dependency> + <dependency> + <code-name-base>org.netbeans.modules.options.api</code-name-base> + <build-prerequisite/> + <compile-dependency/> + <run-dependency> + <release-version>1</release-version> + <specification-version>1.31.2</specification-version> + </run-dependency> + </dependency> + <dependency> + <code-name-base>org.netbeans.modules.settings</code-name-base> + <build-prerequisite/> + <compile-dependency/> + <run-dependency> + <release-version>1</release-version> + <specification-version>1.38.2</specification-version> + </run-dependency> + </dependency> + <dependency> + <code-name-base>org.openide.awt</code-name-base> + <build-prerequisite/> + <compile-dependency/> + <run-dependency> + <specification-version>7.55.1</specification-version> + </run-dependency> + </dependency> + <dependency> + <code-name-base>org.openide.dialogs</code-name-base> + <build-prerequisite/> + <compile-dependency/> + <run-dependency> + <specification-version>7.28.1</specification-version> + </run-dependency> + </dependency> + <dependency> + <code-name-base>org.openide.explorer</code-name-base> + <build-prerequisite/> + <compile-dependency/> + <run-dependency> + <specification-version>6.50.3</specification-version> + </run-dependency> + </dependency> + <dependency> + <code-name-base>org.openide.modules</code-name-base> + <build-prerequisite/> + <compile-dependency/> + <run-dependency> + <specification-version>7.35.1</specification-version> + </run-dependency> + </dependency> + <dependency> + <code-name-base>org.openide.nodes</code-name-base> + <build-prerequisite/> + <compile-dependency/> + <run-dependency> + <specification-version>7.33.2</specification-version> + </run-dependency> + </dependency> + <dependency> + <code-name-base>org.openide.util</code-name-base> + <build-prerequisite/> + <compile-dependency/> + <run-dependency> + <specification-version>8.29.3</specification-version> + </run-dependency> + </dependency> + <dependency> + <code-name-base>org.openide.util.lookup</code-name-base> + <build-prerequisite/> + <compile-dependency/> + <run-dependency> + <specification-version>8.19.1</specification-version> + </run-dependency> + </dependency> + <dependency> + <code-name-base>org.openide.windows</code-name-base> + <build-prerequisite/> + <compile-dependency/> + <run-dependency> + <specification-version>6.60.1</specification-version> + </run-dependency> + </dependency> + <dependency> + <code-name-base>org.sleuthkit.autopsy.core</code-name-base> + <build-prerequisite/> + <compile-dependency/> + <run-dependency> + <release-version>10</release-version> + <specification-version>10.0.11</specification-version> + </run-dependency> + </dependency> + <dependency> + <code-name-base>org.sleuthkit.autopsy.corelibs</code-name-base> + <build-prerequisite/> + <compile-dependency/> + <run-dependency> + <release-version>3</release-version> + <specification-version>1.1</specification-version> + </run-dependency> + </dependency> + </module-dependencies> + <public-packages/> + <class-path-extension> + <runtime-relative-path>ext/controlsfx-8.0.6.jar</runtime-relative-path> + <binary-origin>release/modules/ext/controlsfx-8.0.6.jar</binary-origin> + </class-path-extension> + <class-path-extension> + <runtime-relative-path>ext/jcip-annotations.jar</runtime-relative-path> + <binary-origin>release/modules/ext/jcip-annotations.jar</binary-origin> + </class-path-extension> + <class-path-extension> + <runtime-relative-path>ext/commons-pool2-2.0-sources.jar</runtime-relative-path> + <binary-origin>release/modules/ext/commons-pool2-2.0-sources.jar</binary-origin> + </class-path-extension> + <class-path-extension> + <runtime-relative-path>ext/commons-pool2-2.0.jar</runtime-relative-path> + <binary-origin>release/modules/ext/commons-pool2-2.0.jar</binary-origin> + </class-path-extension> + <class-path-extension> + <runtime-relative-path>ext/sqlite-jdbc-3.7.8-SNAPSHOT.jar</runtime-relative-path> + <binary-origin>release/modules/ext/sqlite-jdbc-3.7.8-SNAPSHOT.jar</binary-origin> + </class-path-extension> + <class-path-extension> + <runtime-relative-path>ext/jcip-annotations-src.jar</runtime-relative-path> + <binary-origin>release/modules/ext/jcip-annotations-src.jar</binary-origin> + </class-path-extension> + <class-path-extension> + <runtime-relative-path>ext/commons-pool2-2.0-javadoc.jar</runtime-relative-path> + <binary-origin>release/modules/ext/commons-pool2-2.0-javadoc.jar</binary-origin> + </class-path-extension> + </data> + </configuration> +</project> diff --git a/ImageAnalyzer/release/modules/ext/commons-pool2-2.0-javadoc.jar b/ImageAnalyzer/release/modules/ext/commons-pool2-2.0-javadoc.jar new file mode 100644 index 0000000000000000000000000000000000000000..1d1e4226b3ae330e68c7a482942f49eab56627ff Binary files /dev/null and b/ImageAnalyzer/release/modules/ext/commons-pool2-2.0-javadoc.jar differ diff --git a/ImageAnalyzer/release/modules/ext/commons-pool2-2.0-sources.jar b/ImageAnalyzer/release/modules/ext/commons-pool2-2.0-sources.jar new file mode 100644 index 0000000000000000000000000000000000000000..2dfe051f95116795fa0976ba1c77ef1a781de728 Binary files /dev/null and b/ImageAnalyzer/release/modules/ext/commons-pool2-2.0-sources.jar differ diff --git a/ImageAnalyzer/release/modules/ext/commons-pool2-2.0.jar b/ImageAnalyzer/release/modules/ext/commons-pool2-2.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..be6d84f92eaf67b9986cc19fc95a5795ef4ff131 Binary files /dev/null and b/ImageAnalyzer/release/modules/ext/commons-pool2-2.0.jar differ diff --git a/ImageAnalyzer/release/modules/ext/controlsfx-8.0.6.jar b/ImageAnalyzer/release/modules/ext/controlsfx-8.0.6.jar new file mode 100644 index 0000000000000000000000000000000000000000..2f2d1badd0facb374d5c2519ce3525ff8693c222 Binary files /dev/null and b/ImageAnalyzer/release/modules/ext/controlsfx-8.0.6.jar differ diff --git a/ImageAnalyzer/release/modules/ext/controlsfx-8.0.6_20.jar b/ImageAnalyzer/release/modules/ext/controlsfx-8.0.6_20.jar new file mode 100644 index 0000000000000000000000000000000000000000..39a73c0b9acb47fb4ab23fc5922313885d0e8e91 Binary files /dev/null and b/ImageAnalyzer/release/modules/ext/controlsfx-8.0.6_20.jar differ diff --git a/ImageAnalyzer/release/modules/ext/jcip-annotations-src.jar b/ImageAnalyzer/release/modules/ext/jcip-annotations-src.jar new file mode 100644 index 0000000000000000000000000000000000000000..bf52a507df2418b68041436fa3df27c4176cc494 Binary files /dev/null and b/ImageAnalyzer/release/modules/ext/jcip-annotations-src.jar differ diff --git a/ImageAnalyzer/release/modules/ext/jcip-annotations.jar b/ImageAnalyzer/release/modules/ext/jcip-annotations.jar new file mode 100644 index 0000000000000000000000000000000000000000..ea263af054bd0e3403fb131c3961e8a4964d07e6 Binary files /dev/null and b/ImageAnalyzer/release/modules/ext/jcip-annotations.jar differ diff --git a/ImageAnalyzer/release/modules/ext/sqlite-jdbc-3.7.8-SNAPSHOT.jar b/ImageAnalyzer/release/modules/ext/sqlite-jdbc-3.7.8-SNAPSHOT.jar new file mode 100644 index 0000000000000000000000000000000000000000..bcea83745ab32246e315dd0c53209dc6a0cd39b9 Binary files /dev/null and b/ImageAnalyzer/release/modules/ext/sqlite-jdbc-3.7.8-SNAPSHOT.jar differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/AutopsyListener.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/AutopsyListener.java new file mode 100644 index 0000000000000000000000000000000000000000..5adbb2512d8d51c92e2a043a6c308a6a86c7b9e6 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/AutopsyListener.java @@ -0,0 +1,165 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import javax.swing.SwingUtilities; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.Content; + +/** Singleton aggregator for listeners that hook into case and ingest modules. + * This class depends on clients to hook up listeners to Autopsy. */ +public class AutopsyListener { + + private static final Logger LOGGER = Logger.getLogger(AutopsyListener.class.getName()); + + private final EurekaController controller = EurekaController.getDefault(); + + private final PropertyChangeListener ingestJobEventListener = new IngestJobEventListener(); + + private final PropertyChangeListener ingestModuleEventListener = new IngestModuleEventListener(); + + private final PropertyChangeListener caseListener = new CaseListener(); + + public PropertyChangeListener getIngestJobEventListener() { + return ingestJobEventListener; + } + + public PropertyChangeListener getIngestModuleEventListener() { + return ingestModuleEventListener; + } + + public PropertyChangeListener getCaseListener() { + return caseListener; + } + + private static AutopsyListener instance; + + private AutopsyListener() { + } + + synchronized public static AutopsyListener getDefault() { + if (instance == null) { + instance = new AutopsyListener(); + } + return instance; + } + + /** listener for ingest events */ + private class IngestJobEventListener implements PropertyChangeListener { + + @Override + synchronized public void propertyChange(PropertyChangeEvent evt) { + switch (IngestManager.IngestJobEvent.valueOf(evt.getPropertyName())) { + case COMPLETED: + if (controller.isListeningEnabled()) { + if (IngestManager.getInstance().isIngestRunning() == false) { + // @@@ Add some logic to not do this if we've done it in the past second + controller.queueTask(controller.new MarkAllFilesAsAnalyzed()); + } + } else { + //TODO can we do anything usefull here? + } + } + } + } + + /** listener for ingest events */ + private class IngestModuleEventListener implements PropertyChangeListener { + + @Override + synchronized public void propertyChange(PropertyChangeEvent evt) { + switch (IngestManager.IngestModuleEvent.valueOf(evt.getPropertyName())) { + + case CONTENT_CHANGED: + //TODO: do we need to do anything here? -jm + break; + case DATA_ADDED: + /* we could listen to DATA events and progressivly update + * files, and get data from DataSource ingest modules, but + * given that most modules don't post new artifacts in the + * events and eureka would have to query for them, without + * knowing which are the new ones, we just ignore these + * events for now. The relevant data should all be captured + * by file done event, anyways -jm */ + break; + case FILE_DONE: + /** getOldValue has fileID, getNewValue has {@link Abstractfile} + * + * {@link IngestManager#fireModuleDataEvent(org.sleuthkit.autopsy.ingest.ModuleDataEvent) fireModuleDataEvent} */ + AbstractFile file = (AbstractFile) evt.getNewValue(); + if (controller.isListeningEnabled()) { + + if (EurekaModule.isSupportedAndNotKnown(file)) { + //this file should be included and we don't already know about it from hash sets (NSRL) + controller.queueTask(controller.new UpdateFile(file)); + } else if (EurekaModule.getAllSupportedExtensions().contains(file.getNameExtension())) { + //doing this check results in fewer tasks queued up, and faster completion of db update + //this file would have gotten scooped up in initial grab, but actually we don't need it + controller.queueTask(controller.new RemoveFile(file)); + } + } else { + controller.setStale(true); + //TODO: keep track of waht we missed for later + } + break; + } + } + } + + /** listener for case events */ + private class CaseListener implements PropertyChangeListener { + + @Override + synchronized public void propertyChange(PropertyChangeEvent evt) { + + switch (Case.Events.valueOf(evt.getPropertyName())) { + case CURRENT_CASE: + Case newCase = (Case) evt.getNewValue(); + if (newCase != null) { // case has been opened + //connect db, groupmanager, start worker thread + controller.setCase(newCase); + + } else { // case is closing + //close eureka window + SwingUtilities.invokeLater(EurekaModule::closeTopComponent); + controller.reset(); + } + break; + + case DATA_SOURCE_ADDED: + //copy all file data to eureka databse + Content newDataSource = (Content) evt.getNewValue(); + if (controller.isListeningEnabled()) { + controller.queueTask(controller.new PrePopulateDataSourceFiles(newDataSource.getId())); + } else { + controller.setStale(true); + //TODO: keep track of what we missed for later + } + break; + } + } + } + + +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/Bundle.properties b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/Bundle.properties new file mode 100644 index 0000000000000000000000000000000000000000..5e5062ea192208f43fc214b3ed8e4429a4b511e9 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/Bundle.properties @@ -0,0 +1,8 @@ +OpenIDE-Module-Long-Description=\ + New image and video analyzer that has been designed to make performing image-intensive investigations more efficient. \ + This work has been funded by DHS S&T and this is a beta release. \ + It is not available on the sleuthkit.org site and has been distributed to limited users. +OpenIDE-Module-Name=Image Analysis (Beta) +OpenIDE-Module-Short-Description=Advanced image and video analyzer +EurekaOptionsPanel.enabledByDefaultBox.text=Enable listening to ingest for new cases. +EurekaOptionsPanel.enabledForCaseBox.text=Enable listening to ingest for current case. diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaController.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaController.java new file mode 100644 index 0000000000000000000000000000000000000000..fe5a680a48f01e0e7c4c26ee031bb43978096925 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaController.java @@ -0,0 +1,877 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.logging.Level; +import javafx.application.Platform; +import javafx.beans.Observable; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; +import javafx.beans.property.ReadOnlyIntegerProperty; +import javafx.beans.property.ReadOnlyIntegerWrapper; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.property.SimpleListProperty; +import javafx.collections.FXCollections; +import javafx.concurrent.Task; +import javafx.concurrent.Worker; +import static javafx.concurrent.Worker.State.CANCELLED; +import static javafx.concurrent.Worker.State.FAILED; +import static javafx.concurrent.Worker.State.READY; +import static javafx.concurrent.Worker.State.RUNNING; +import static javafx.concurrent.Worker.State.SCHEDULED; +import static javafx.concurrent.Worker.State.SUCCEEDED; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import net.jcip.annotations.GuardedBy; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +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.coreutils.Logger; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.Category; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableDB; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile; +import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupKey; +import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupManager; +import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupViewState; +import org.sleuthkit.autopsy.imageanalyzer.grouping.Grouping; +import org.sleuthkit.autopsy.imageanalyzer.gui.EurekaToolbar; +import org.sleuthkit.autopsy.imageanalyzer.gui.NoGroupsDialog; +import org.sleuthkit.autopsy.imageanalyzer.gui.SummaryTablePane; +import org.sleuthkit.autopsy.imageanalyzer.progress.ProgressAdapterBase; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.TskData; + +/** + * Acts as the controller in GroupManager - GroupListPane - EurekaController MVC + * Trio + * + * Connects different parts of Eureka together and is hub for flow of control. + */ +public class EurekaController implements FileUpdateListener { + + private static final Logger LOGGER = Logger.getLogger(EurekaController.class.getName()); + + private final Region infoOverLayBackground = new Region() { + { + setBackground(new Background(new BackgroundFill(Color.GREY, CornerRadii.EMPTY, Insets.EMPTY))); + setOpacity(.4); + } + }; + + private static EurekaController instance; + + public static synchronized EurekaController getDefault() { + if (instance == null) { + instance = new EurekaController(); + } + return instance; + } + + /** list based stack to hold history, 'top' is at index 0; */ + @javax.annotation.concurrent.GuardedBy("this") + private final ObservableStack<GroupViewState> historyStack = new ObservableStack<>(); + + @javax.annotation.concurrent.GuardedBy("this") + private final ObservableStack<GroupViewState> forwardStack = new ObservableStack<>(); + + private final ReadOnlyBooleanWrapper listeningEnabled = new ReadOnlyBooleanWrapper(false); + + private final ReadOnlyIntegerWrapper queueSizeProperty = new ReadOnlyIntegerWrapper(0); + + private final ReadOnlyBooleanWrapper regroupDisabled = new ReadOnlyBooleanWrapper(false); + + private final ReadOnlyBooleanWrapper stale = new ReadOnlyBooleanWrapper(false); + + private final ReadOnlyBooleanWrapper metaDataCollapsed = new ReadOnlyBooleanWrapper(false); + + private final FileIDSelectionModel selectionModel = FileIDSelectionModel.getInstance(); + + private DBWorkerThread dbWorkerThread; + + private DrawableDB db; + + private final GroupManager groupManager = new GroupManager(this); + + private StackPane fullUIStackPane; + + private StackPane centralStackPane; + + private Node infoOverlay; + + synchronized public ObservableStack<GroupViewState> getHistoryStack() { + return historyStack; + } + + synchronized public ObservableStack<GroupViewState> getForwardStack() { + return forwardStack; + } + + public ReadOnlyBooleanProperty getMetaDataCollapsed() { + return metaDataCollapsed.getReadOnlyProperty(); + } + + public void setMetaDataCollapsed(Boolean metaDataCollapsed) { + this.metaDataCollapsed.set(metaDataCollapsed); + } + + private GroupViewState getViewState() { + return viewState.get(); + } + + public ReadOnlyBooleanProperty regroupDisabled() { + return regroupDisabled.getReadOnlyProperty(); + } + + private final ReadOnlyObjectWrapper<GroupViewState> viewState = new ReadOnlyObjectWrapper<>(); + + private void setViewState(GroupViewState newViewState) { + viewState.set(newViewState); + } + + public ReadOnlyObjectProperty<GroupViewState> viewState() { + return viewState.getReadOnlyProperty(); + } + + /** + * the list of tasks queued to run in the uiBGTaskExecutor. By keeping this + * list we can cancel them more gracefully than by {@link ExecutorService#shutdownNow() + */ + @GuardedBy("bgTasks") + private final SimpleListProperty<Future<?>> bgTasks = new SimpleListProperty<>(FXCollections.observableArrayList()); + + /** an executor to submit async ui related background tasks to. */ + final ExecutorService bgTaskExecutor = Executors.newSingleThreadExecutor(new BasicThreadFactory.Builder().namingPattern("ui task -%d").build()); + + public synchronized FileIDSelectionModel getSelectionModel() { + + return selectionModel; + } + + public GroupManager getGroupManager() { + return groupManager; + } + + public void setListeningEnabled(boolean enabled) { + listeningEnabled.set(enabled); + } + + ReadOnlyBooleanProperty listeningEnabled() { + return listeningEnabled.getReadOnlyProperty(); + } + + boolean isListeningEnabled() { + return listeningEnabled.get(); + } + + void setStale(Boolean b) { + Platform.runLater(() -> { + stale.set(b); + }); + if (Case.isCaseOpen()) { + new PerCaseProperties(Case.getCurrentCase()).setConfigSetting(EurekaModule.MODULE_NAME, PerCaseProperties.STALE, b.toString()); + } + } + + public ReadOnlyBooleanProperty stale() { + return stale.getReadOnlyProperty(); + } + + boolean isStale() { + return stale.get(); + } + + private EurekaController() { + + listeningEnabled.addListener((observable, oldValue, newValue) -> { + if (newValue && !oldValue) { + queueTask(new CopyAnalyzedFiles()); + } + }); + + groupManager.getAnalyzedGroups().addListener((Observable o) -> { + checkForGroups(); + }); + + groupManager.getUnSeenGroups().addListener((Observable observable) -> { + if (groupManager.getUnSeenGroups().size() > 0 && (getViewState() == null || getViewState().getGroup() == null)) { + setViewState(GroupViewState.tile(groupManager.getUnSeenGroups().get(0))); + } + }); + regroupDisabled.addListener((Observable observable) -> { + checkForGroups(); + }); + + IngestManager.getInstance().addIngestModuleEventListener((evt) -> { + Platform.runLater(this::updateRegroupDisabled); + }); + IngestManager.getInstance().addIngestJobEventListener((evt) -> { + Platform.runLater(this::updateRegroupDisabled); + }); +// metaDataCollapsed.bind(EurekaToolbar.getDefault().showMetaDataProperty()); + } + + /** submit a background {@link Task} to be queued for execution + * by the thread pool. + * + * @param task */ + public void submitBGTask(final Task<?> task) { + //listen to task state and remove task from list of tasks once it is 'done' + task.stateProperty().addListener((observableState, oldState, newState) -> { + switch (newState) { + case READY: + case SCHEDULED: + case RUNNING: + break; + + case FAILED: + LOGGER.log(Level.WARNING, "task :" + task.getTitle() + " failed", task.getException()); + case CANCELLED: + case SUCCEEDED: + Platform.runLater(() -> { + synchronized (bgTasks) { + bgTasks.remove(task); + } + }); + break; + } + }); + + synchronized (bgTasks) { + bgTasks.add(task); + } + + bgTaskExecutor.execute(task); + } + + synchronized public void pushGroup(GroupViewState viewState, boolean force) { + final GroupViewState currentViewState = getViewState(); + + if (force || currentViewState.equals(viewState) == false) { + historyStack.push(currentViewState); + setViewState(viewState); + if (viewState.equals(forwardStack.peek())) { + forwardStack.pop(); + } else { + forwardStack.clear(); + } + } + } + + synchronized public GroupViewState goForward() { + + final GroupViewState currentZoom = getViewState(); + + final GroupViewState fpeek = forwardStack.peek(); + + if (fpeek != null && currentZoom.equals(fpeek) == false) { + historyStack.push(currentZoom); + setViewState(fpeek); + forwardStack.pop(); + } + return fpeek; + } + + synchronized public GroupViewState goBack() { + final GroupViewState currentZoom = getViewState(); + final GroupViewState peek = historyStack.peek(); + + if (peek != null && peek.equals(currentZoom) == false) { + forwardStack.push(currentZoom); + setViewState(historyStack.pop()); + } else if (peek != null && peek.equals(currentZoom)) { + historyStack.pop(); + return goBack(); + } + return peek; + } + + private void updateRegroupDisabled() { + regroupDisabled.set(getFileUpdateQueueSizeProperty().get() > 0 || IngestManager.getInstance().isIngestRunning()); + } + + /** Check if there are any fully analyzed groups available + * from the GroupManager and remove blocking progress spinners if there + * are. If there aren't, add a blocking progress spinner with appropriate + * message. */ + public final void checkForGroups() { + if (groupManager.getAnalyzedGroups().isEmpty()) { + if (IngestManager.getInstance().isIngestRunning()) { + if (listeningEnabled.get() == false) { + replaceNotification(fullUIStackPane, + new NoGroupsDialog("No groups are fully analyzed but listening to ingest is disabled. " + + " No groups will be available until ingest is finished and listening is re-enabled.")); + } else { + replaceNotification(fullUIStackPane, + new NoGroupsDialog("No groups are fully analyzed yet, but ingest is still ongoing. Please Wait.", + new ProgressIndicator())); + } + + } else if (getFileUpdateQueueSizeProperty().get() > 0) { + replaceNotification(fullUIStackPane, + new NoGroupsDialog("No groups are fully analyzed yet, but image / video data is still being populated. Please Wait.", + new ProgressIndicator())); + } else if (db != null && db.countAllFiles() <= 0) { // there are no files in db + replaceNotification(fullUIStackPane, + new NoGroupsDialog("There are no images/videos in the added datasources.")); + + } else if (!groupManager.isRegrouping()) { + replaceNotification(centralStackPane, + new NoGroupsDialog("There are no fully analyzed groups to display:" + + " the current Group By setting resulted in no groups, " + + "or no groups are fully analyzed but ingest is not running.")); + } +// else { +// replaceNotification(fullUIStackPane, +// new NoGroupsDialog("Please wait while the images/videos are re grouped.", +// new ProgressIndicator())); +// +// } + // } + + } else { + clearNotification(); + } + } + + private void clearNotification() { + //remove the ingest spinner + if (fullUIStackPane != null) { + fullUIStackPane.getChildren().remove(infoOverlay); + } + //remove the ingest spinner + if (centralStackPane != null) { + centralStackPane.getChildren().remove(infoOverlay); + } + } + + private void replaceNotification(StackPane stackPane, Node newNode) { + clearNotification(); + + infoOverlay = new StackPane(infoOverLayBackground, newNode); + if (stackPane != null) { + stackPane.getChildren().add(infoOverlay); + } + } + + private void restartWorker() { + if (dbWorkerThread != null) { + dbWorkerThread.cancelAllTasks(); + } + dbWorkerThread = new DBWorkerThread(); + + getFileUpdateQueueSizeProperty().addListener((o) -> { + Platform.runLater(this::updateRegroupDisabled); + }); + + Thread th = new Thread(dbWorkerThread); + th.setDaemon(false); // we want it to go away when it is done + th.start(); + } + + /** + * initialize the controller for a specific case. + * + * @param c + */ + public synchronized void setCase(Case c) { + + this.db = DrawableDB.getDrawableDB(c.getCaseDirectory(), this); + db.addUpdatedFileListener(this); + setListeningEnabled(EurekaModule.isEnabledforCase(c)); + setStale(EurekaModule.isCaseStale(c)); + + // if we add this line icons are made as files are analyzed rather than on demand. + // db.addUpdatedFileListener(IconCache.getDefault()); + restartWorker(); + + groupManager.setDB(db); + SummaryTablePane.getDefault().handleCategoryChanged(Collections.EMPTY_LIST); + } + + /** + * handle {@link FileUpdateEvent} sent from Db when files are + * inserted/updated + * + * @param evt + */ + @Override + synchronized public void handleFileUpdate(FileUpdateEvent evt) { + final Collection<Long> fileIDs = evt.getUpdatedFiles(); + switch (evt.getUpdateType()) { + case FILE_REMOVED: + for (final long fileId : fileIDs) { + //get grouping(s) this file would be in + Set<GroupKey> groupsForFile = groupManager.getGroupKeysForFileID(fileId); + + for (GroupKey gk : groupsForFile) { + groupManager.removeFromGroup(gk, fileId); + } + } + + break; + case FILE_UPDATED: + + /** + * TODO: is there a way to optimize this to avoid quering to db + * so much. the problem is that as a new files are analyzed they + * might be in new groups( if we are grouping by say make or + * model) + * + * TODO: Should this be a InnerTask so it can be done by the + * WorkerThread? Is it already done by worker thread because + * handlefileUpdate is invoked through call on db in UpdateTask + * innertask? -jm + */ + for (final long fileId : fileIDs) { + + //get grouping(s) this file would be in + Set<GroupKey> groupsForFile = groupManager.getGroupKeysForFileID(fileId); + + for (GroupKey gk : groupsForFile) { + Grouping g = groupManager.getGroupForKey(gk); + + //if there is aleady a group that was previously deemed fully analyzed, then add this newly analyzed file to it. + if (g != null) { + g.addFile(fileId); + } ////if there wasn't already a group check if there should be one now + else { + //TODO: use method in groupmanager ? + List<Long> checkAnalyzed = groupManager.checkAnalyzed(gk); + if (checkAnalyzed != null) { // => the group is analyzed, so add it to the ui + groupManager.populateAnalyzedGroup(gk, checkAnalyzed); + } + } + } + } + + Category.fireChange(fileIDs); + if (evt.getChangedAttribute() == DrawableAttribute.TAGS) { + TagUtils.fireChange(fileIDs); + } + break; + } + } + + /** + * reset the state of the controller (eg if the case is closed) + */ + public synchronized void reset() { + LOGGER.info("resetting EurekaControler to initial state."); + selectionModel.clearSelection(); + Platform.runLater(() -> { + viewState.set(null); + }); + + EurekaToolbar.getDefault().reset(); + groupManager.clear(); + if (db != null) { + db.closeDBCon(); + } + db = null; + } + + /** + * add InnerTask to the queue that the worker thread gets its work from + * + * @param innerTask + */ + final void queueTask(InnerTask innerTask) { + // @@@ We could make a lock for the worker thread + if (dbWorkerThread == null) { + restartWorker(); + } + dbWorkerThread.addTask(innerTask); + } + + public DrawableFile getFileFromId(Long fileID) throws TskCoreException { + return db.getFileFromID(fileID); + } + + public void setStacks(StackPane fullUIStack, StackPane centralStack) { + fullUIStackPane = fullUIStack; + this.centralStackPane = centralStack; + Platform.runLater(this::checkForGroups); + } + + public final ReadOnlyIntegerProperty getFileUpdateQueueSizeProperty() { + return queueSizeProperty.getReadOnlyProperty(); + } + + public ReadOnlyIntegerProperty bgTaskQueueSizeProperty() { + return bgTasks.sizeProperty(); + } + + // @@@ REVIEW IF THIS SHOLD BE STATIC... + //TODO: concept seems like the controller deal with how much work to do at a given time + // @@@ review this class for synchronization issues (i.e. reset and cancel being called, add, etc.) + private class DBWorkerThread implements Runnable { + + // true if the process was requested to stop. Currently no way to reset it + private volatile boolean cancelled = false; + + // list of tasks to run + private final BlockingQueue<InnerTask> workQueue = new LinkedBlockingQueue(); + + /** + * Cancel all of the queued up tasks and the currently scheduled task. + * Note that after you cancel, you cannot submit new jobs to this + * thread. + */ + public void cancelAllTasks() { + cancelled = true; + for (InnerTask it : workQueue) { + it.cancel(); + } + workQueue.clear(); + queueSizeProperty.set(workQueue.size()); + } + + /** + * Add a task for the worker thread to perform + * + * @param it + */ + public void addTask(InnerTask it) { + workQueue.add(it); + queueSizeProperty.set(workQueue.size()); + } + + @Override + public void run() { + // nearly infinite loop waiting for tasks + while (true) { + if (cancelled) { + return; + } + try { + // @@@ Could probably do something more fancy here and check if we've been canceled every now and then + InnerTask it = workQueue.take(); + if (it.cancelled == false) { + it.run(); + } + + queueSizeProperty.set(workQueue.size()); + + } catch (InterruptedException ex) { + Exceptions.printStackTrace(ex); + } + + } + } + } + + public SleuthkitCase getSleuthKitCase() throws IllegalStateException { + if (Case.isCaseOpen()) { + return Case.getCurrentCase().getSleuthkitCase(); + } else { + throw new IllegalStateException("No Case is open!"); + } + } + + /** + * Abstract base class for task to be done on {@link DBWorkerThread} + */ + static public abstract class InnerTask extends ProgressAdapterBase implements Runnable { + + protected volatile boolean cancelled = false; + + public void cancel() { + updateState(Worker.State.CANCELLED); + } + + protected boolean isCancelled() { + return getState() == Worker.State.CANCELLED; + } + } + + /** + * Abstract base class for tasks associated with an obj id in the database + */ + static private abstract class TaskWithID extends InnerTask { + + protected Long obj_id; // id of image or file + + public TaskWithID(Long id) { + super(); + this.obj_id = id; + } + + public Long getId() { + return obj_id; + } + } + + /** + * Task to mark all unanalyzed files in the DB as analyzed. Just to make + * sure that all are displayed. Added because there were rare cases where + * something failed and a file was never marked as analyzed and therefore + * never displayed. This task should go into the queue at the end after all + * of the update tasks. + */ + class MarkAllFilesAsAnalyzed extends InnerTask { + + @Override + public void run() { + db.markAllFilesAnalyzed(); +// checkForGroups(); + } + } + + /** + * task that updates one file in database with results from ingest + */ + class UpdateFile extends InnerTask { + + private final AbstractFile file; + + public UpdateFile(AbstractFile f) { + super(); + this.file = f; + } + + /** + * Update a file in the database + */ + @Override + public void run() { + DrawableFile drawableFile = DrawableFile.create(file, true); + db.updateFile(drawableFile); + } + } + + /** + * task that updates one file in database with results from ingest + */ + class RemoveFile extends InnerTask { + + private final AbstractFile file; + + public RemoveFile(AbstractFile f) { + super(); + this.file = f; + } + + /** + * Update a file in the database + */ + @Override + public void run() { + boolean removeFile = db.removeFile(file.getId()); + } + } + + /** Task that runs when eureka listening is (re) enabled. + * + * Uses the presence of TSK_FILE_TYPE_SIG attributes as a approximation to + * 'analyzed'. Grabs all files with supported image/video mime types, and + * adds + * them to the Drawable DB */ + class CopyAnalyzedFiles extends InnerTask { + + final private String DRAWABLE_QUERY = "name LIKE '%." + StringUtils.join(EurekaModule.getAllSupportedExtensions(), "' or name LIKE '%.") + "'"; + + private ProgressHandle progressHandle = ProgressHandleFactory.createHandle("populating analyzed image/video database"); + + @Override + public void run() { + progressHandle.start(); + updateMessage("populating analyzed image/video database"); + + try { + //grap all files with supported mime types + final List<AbstractFile> files = getSleuthKitCase().findAllFilesWhere(DRAWABLE_QUERY + " or tsk_files.obj_id in (select tsk_files.obj_id from tsk_files , blackboard_artifacts, blackboard_attributes" + + " where blackboard_artifacts.obj_id = tsk_files.obj_id" + + " and blackboard_attributes.artifact_id = blackboard_artifacts.artifact_id" + + " and blackboard_artifacts.artifact_type_id = " + BlackboardArtifact.ARTIFACT_TYPE.TSK_GEN_INFO.getTypeID() + + " and blackboard_attributes.attribute_type_id = " + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_FILE_TYPE_SIG.getTypeID() + + " and blackboard_attributes.value_text in ('" + StringUtils.join(EurekaModule.getSupportedMimes(), "','") + "'))"); + progressHandle.switchToDeterminate(files.size()); + + updateProgress(0.0); + + //do in transaction + DrawableDB.DrawableTransaction tr = db.beginTransaction(); + int units = 0; + for (final AbstractFile f : files) { + if (cancelled) { + LOGGER.log(Level.WARNING, "task cancelled: not all contents may be transfered to database"); + progressHandle.finish(); + break; + } + final Boolean hasMimeType = EurekaModule.hasSupportedMimeType(f); + final boolean known = f.getKnown() == TskData.FileKnown.KNOWN; + + if (known) { + db.removeFile(f.getId(), tr); //remove known files + } else { + if (hasMimeType == null) { + if (EurekaModule.isSupported(f)) { + //no mime type but supported => not add as not analyzed + db.updatefile(DrawableFile.create(f, false), tr); + } else { + //no mime type, not supported => remove ( how dd we get here) + db.removeFile(f.getId(), tr); + } + } else { + if (hasMimeType) { // supported mimetype => analyzed + db.updatefile(DrawableFile.create(f, true), tr); + } else { //unsupported mimtype => analyzed but shouldn't include + db.removeFile(f.getId(), tr); + } + } + } + + units++; + final int prog = units; + progressHandle.progress(f.getName(), units); + updateProgress(prog - 1 / (double) files.size()); + updateMessage(f.getName()); + } + + progressHandle.finish(); + + progressHandle = ProgressHandleFactory.createHandle("commiting image/video database"); + updateMessage("commiting image/video database"); + updateProgress(1.0); + + progressHandle.start(); + db.commitTransaction(tr, true); + + } catch (TskCoreException ex) { + Logger.getLogger(PrePopulateDataSourceFiles.class.getName()).log(Level.WARNING, "failed to transfer all database contents", ex); + } catch (IllegalStateException ex) { + Logger.getLogger(PrePopulateDataSourceFiles.class.getName()).log(Level.SEVERE, "Case was closed out from underneath CopyDataSource task", ex); + } + + progressHandle.finish(); + + updateMessage( + ""); + updateProgress( + -1.0); + setStale(false); + } + + } + + /** task that does pre-ingest copy over of files from a new datasource + * with + * (uses fs_obj_id to identify files from new datasource) * + * + * TODO: create methods to simplify progress value/text updates to both + * netbeans and eureka progress/status */ + class PrePopulateDataSourceFiles extends TaskWithID { + + /** @TODO: for initial grab is there any better way than by + * extension? + * + * in file_done listener we look at file type id attributes and fall + * back on jpeg signatures and extensions to check for supported + * images */ + // (name like '.jpg' or name like '.png' ...) + final private String DRAWABLE_QUERY = "name LIKE '%." + StringUtils.join(EurekaModule.getAllSupportedExtensions(), "' or name LIKE '%.") + "'"; + + private ProgressHandle progressHandle = ProgressHandleFactory.createHandle("prepopulating image/video database"); + + public PrePopulateDataSourceFiles(Long id) { + super(id); + } + + /** Copy files from a newly added data source into the DB */ + @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<AbstractFile> files; + try { + files = getSleuthKitCase().findAllFilesWhere(DRAWABLE_QUERY + "and fs_obj_id = " + this.obj_id); + progressHandle.switchToDeterminate(files.size()); + + updateProgress(0.0); + + //do in transaction + DrawableDB.DrawableTransaction tr = db.beginTransaction(); + int units = 0; + for (final AbstractFile f : files) { + if (cancelled) { + LOGGER.log(Level.WARNING, "task cancelled: not all contents may be transfered to database"); + progressHandle.finish(); + break; + } + db.updatefile(DrawableFile.create(f, false), tr); + units++; + final int prog = units; + progressHandle.progress(f.getName(), units); + updateProgress(prog - 1 / (double) files.size()); + updateMessage(f.getName()); + } + + progressHandle.finish(); + progressHandle = ProgressHandleFactory.createHandle("commiting image/video database"); + updateMessage("commiting image/video database"); + updateProgress(1.0); + + progressHandle.start(); + db.commitTransaction(tr, false); + + } catch (TskCoreException ex) { + Logger.getLogger(PrePopulateDataSourceFiles.class.getName()).log(Level.WARNING, "failed to transfer all database contents", ex); + } catch (IllegalStateException ex) { + Logger.getLogger(PrePopulateDataSourceFiles.class.getName()).log(Level.SEVERE, "Case was closed out from underneath CopyDataSource task", ex); + } + + progressHandle.finish(); + + updateMessage(""); + updateProgress(-1.0); + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaModule.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaModule.java new file mode 100644 index 0000000000000000000000000000000000000000..3f41e27e535f398a3da780eaf8bbc7cea8494cb2 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaModule.java @@ -0,0 +1,192 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-14 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Sets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import java.util.logging.Level; +import javax.imageio.ImageIO; +import org.apache.commons.lang3.StringUtils; +import org.openide.windows.Mode; +import org.openide.windows.TopComponent; +import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.ImageUtils; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableDB; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.TskData; + +/** static definitions and utilities for the Eureka Module + * + */ +public class EurekaModule { + + static private final Logger LOGGER = Logger.getLogger(EurekaModule.class.getName()); + + static final String MODULE_NAME = EurekaModule.class.getSimpleName(); + + static private final Set<String> videoExtensions + = Sets.newHashSet("aaf", "3gp", "asf", "avi", "m1v", "m2v", "m4v", "mp4", + "mov", "mpeg", "mpg", "mpe", "mp4", "rm", "wmv", "mpv", + "flv", "swf"); + + static private final Set<String> imageExtensions = Sets.newHashSet(ImageIO.getReaderFileSuffixes()); + + static private final Set<String> supportedExtensions = Sets.union(imageExtensions, videoExtensions); + + static private final Set<String> imageMimes = Sets.newHashSet("image/jpeg", "image/bmp", "image/gif", "image/png"); + + static private final Set<String> videoMimes = Sets.newHashSet("video/mp4", "video/x-flv", "video/x-javafx"); + + static private final Set<String> supportedMimes = Sets.union(imageMimes, videoMimes); + + public static Set<String> getSupportedMimes() { + return Collections.unmodifiableSet(supportedMimes); + } + + private EurekaModule() { + } + + static boolean isEnabledforCase(Case c) { + if (c != null) { + String enabledforCaseProp = new PerCaseProperties(c).getConfigSetting(EurekaModule.MODULE_NAME, PerCaseProperties.ENABLED); + return StringUtils.isNotBlank(enabledforCaseProp) ? Boolean.valueOf(enabledforCaseProp) : EurekaPreferences.isEnabledByDefault(); + } else { + return false; + } + } + + public static boolean isCaseStale(Case c) { + if (c != null) { + String stale = new PerCaseProperties(c).getConfigSetting(EurekaModule.MODULE_NAME, PerCaseProperties.STALE); + return StringUtils.isNotBlank(stale) ? Boolean.valueOf(stale) : false; + } else { + return false; + } + } + + public static Set<String> getAllSupportedExtensions() { + return supportedExtensions; + } + + public static Boolean isSupported(AbstractFile file) { + //if there were no file type attributes, or we failed to read it, fall back on extension and jpeg header + return Optional.ofNullable(hasSupportedMimeType(file)).orElseGet( + () -> supportedExtensions.contains(getFileExtension(file)) || ImageUtils.isJpegFileHeader(file)); + } + + /** + * + * @param file + * + * @return true if the file had a TSK_FILE_TYPE_SIG attribute on a + * TSK_GEN_INFO that is in the supported list. False if there was an + * unsupported attribute, null if no attributes were found + */ + public static Boolean hasSupportedMimeType(AbstractFile file) { + try { + ArrayList<BlackboardAttribute> fileSignatureAttrs = file.getGenInfoAttributes(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_FILE_TYPE_SIG); + if (fileSignatureAttrs.isEmpty() == false) { + return fileSignatureAttrs.stream().anyMatch(attr -> supportedMimes.contains(attr.getValueString())); + } + } catch (TskCoreException ex) { + LOGGER.log(Level.INFO, "failed to read TSK_FILE_TYPE_SIG attribute for " + file.getName(), ex); + } + return null; + } + + /** @param file + * + * @return + */ + public static boolean isVideoFile(AbstractFile file) { + try { + ArrayList<BlackboardAttribute> fileSignatureAttrs = file.getGenInfoAttributes(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_FILE_TYPE_SIG); + if (fileSignatureAttrs.isEmpty() == false) { + return fileSignatureAttrs.stream().anyMatch(attr -> videoMimes.contains(attr.getValueString())); + } + } catch (TskCoreException ex) { + LOGGER.log(Level.INFO, "failed to read TSK_FILE_TYPE_SIG attribute for " + file.getName(), ex); + } + //if there were no file type attributes, or we failed to read it, fall back on extension + return videoExtensions.contains(getFileExtension(file)); + } + + private static String getFileExtension(AbstractFile file) { + return Iterables.getLast(Arrays.asList(StringUtils.split(file.getName(), '.')), ""); + } + + /** + * Is the given file 'supported' and not 'known'(nsrl hash hit). If so we + * should include it in {@link DrawableDB} and UI + * + * @param abstractFile + * + * @return true if the given {@link AbstractFile} is 'supported' and not + * 'known', else false + */ + static public boolean isSupportedAndNotKnown(AbstractFile abstractFile) { + return (abstractFile.getKnown() != TskData.FileKnown.KNOWN) && EurekaModule.isSupported(abstractFile); + } + + //TODO: this doesn ot really belong here, move it to EurekaController? Module? + @ThreadConfined(type = ThreadConfined.ThreadType.UI) + public static void closeTopComponent() { + final TopComponent etc = WindowManager.getDefault().findTopComponent("EurekaTopComponent"); + if (etc != null) { + try { + etc.close(); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "failed to close EurekaTopComponent", e); + } + } + } + + @ThreadConfined(type = ThreadConfined.ThreadType.UI) + public static void openTopComponent() { + //TODO:eventually move to this model, throwing away everything and rebuilding controller groupmanager etc for each case. +// synchronized (OpenTimelineAction.class) { +// if (timeLineController == null) { +// timeLineController = new TimeLineController(); +// LOGGER.log(Level.WARNING, "Failed to get TimeLineController from lookup. Instantiating one directly.S"); +// } +// } +// timeLineController.openTimeLine(); + final EurekaTopComponent EurekaTc = (EurekaTopComponent) WindowManager.getDefault().findTopComponent("EurekaTopComponent"); + if (EurekaTc != null) { + WindowManager.getDefault().isTopComponentFloating(EurekaTc); + Mode mode = WindowManager.getDefault().findMode("timeline"); + if (mode != null) { + mode.dockInto(EurekaTc); + + } + EurekaTc.open(); + EurekaTc.requestActive(); + } + } + +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaOptionsPanel.form b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaOptionsPanel.form new file mode 100644 index 0000000000000000000000000000000000000000..e811ca7e4002a750ab16d439073342f753cdd511 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaOptionsPanel.form @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.5" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + </AuxValues> + + <Layout> + <DimensionLayout dim="0"> + <Group type="103" groupAlignment="0" attributes="0"> + <Group type="102" attributes="0"> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="0" attributes="0"> + <Component id="enabledForCaseBox" alignment="0" min="-2" max="-2" attributes="0"/> + <Component id="enabledByDefaultBox" alignment="0" min="-2" max="-2" attributes="0"/> + </Group> + <EmptySpace max="32767" attributes="0"/> + </Group> + </Group> + </DimensionLayout> + <DimensionLayout dim="1"> + <Group type="103" groupAlignment="0" attributes="0"> + <Group type="102" alignment="0" attributes="0"> + <EmptySpace max="-2" attributes="0"/> + <Component id="enabledByDefaultBox" min="-2" max="-2" attributes="0"/> + <EmptySpace type="separate" max="-2" attributes="0"/> + <Component id="enabledForCaseBox" min="-2" max="-2" attributes="0"/> + <EmptySpace max="32767" attributes="0"/> + </Group> + </Group> + </DimensionLayout> + </Layout> + <SubComponents> + <Component class="javax.swing.JCheckBox" name="enabledByDefaultBox"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="org/sleuthkit/autopsy/imageanalyzer/Bundle.properties" key="EurekaOptionsPanel.enabledByDefaultBox.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + </Property> + </Properties> + </Component> + <Component class="javax.swing.JCheckBox" name="enabledForCaseBox"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="org/sleuthkit/autopsy/imageanalyzer/Bundle.properties" key="EurekaOptionsPanel.enabledForCaseBox.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + </Property> + </Properties> + </Component> + </SubComponents> +</Form> diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaOptionsPanel.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaOptionsPanel.java new file mode 100644 index 0000000000000000000000000000000000000000..7c6d7d35273c8101457e33e9cf108dffc5bbf150 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaOptionsPanel.java @@ -0,0 +1,115 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer; + +import java.awt.event.ActionEvent; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.ingest.IngestManager; + +/** The Image/Video Analyzer panel in the NetBeans provided Options Dialogs + * accessed via Tool -> Options */ +final class EurekaOptionsPanel extends javax.swing.JPanel { + + EurekaOptionsPanel(EurekaOptionsPanelController controller) { + initComponents(); + + IngestManager.getInstance().addIngestJobEventListener(evt -> { + enabledForCaseBox.setEnabled(Case.isCaseOpen() && IngestManager.getInstance().isIngestRunning() == false); + }); + + enabledByDefaultBox.addActionListener((ActionEvent e) -> { + controller.changed(); + }); + + enabledForCaseBox.addActionListener((ActionEvent e) -> { + controller.changed(); + }); + } + + /** 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. + */ + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + + enabledByDefaultBox = new javax.swing.JCheckBox(); + enabledForCaseBox = new javax.swing.JCheckBox(); + + org.openide.awt.Mnemonics.setLocalizedText(enabledByDefaultBox, org.openide.util.NbBundle.getMessage(EurekaOptionsPanel.class, "EurekaOptionsPanel.enabledByDefaultBox.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(enabledForCaseBox, org.openide.util.NbBundle.getMessage(EurekaOptionsPanel.class, "EurekaOptionsPanel.enabledForCaseBox.text")); // NOI18N + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(enabledForCaseBox) + .addComponent(enabledByDefaultBox)) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(enabledByDefaultBox) + .addGap(18, 18, 18) + .addComponent(enabledForCaseBox) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + }// </editor-fold>//GEN-END:initComponents + + /** {@inheritDoc} */ + void load() { + enabledByDefaultBox.setSelected(EurekaPreferences.isEnabledByDefault()); + if (Case.isCaseOpen() && IngestManager.getInstance().isIngestRunning() == false) { + enabledForCaseBox.setEnabled(true); + enabledForCaseBox.setSelected(EurekaModule.isEnabledforCase(Case.getCurrentCase())); + } else { + enabledForCaseBox.setEnabled(false); + enabledForCaseBox.setSelected(enabledByDefaultBox.isSelected()); + } + } + + /** {@inheritDoc } */ + void store() { + EurekaPreferences.setEnabledByDefault(enabledByDefaultBox.isSelected()); + EurekaController.getDefault().setListeningEnabled(enabledForCaseBox.isSelected()); + if (Case.isCaseOpen()) { + new PerCaseProperties(Case.getCurrentCase()).setConfigSetting(EurekaModule.MODULE_NAME, PerCaseProperties.ENABLED, Boolean.toString(enabledForCaseBox.isSelected())); + } + } + + /** {@inheritDoc } + * + * @return true, since there is no way for this form to be invalid */ + boolean valid() { + // TODO check whether form is consistent and complete + return true; + } + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JCheckBox enabledByDefaultBox; + private javax.swing.JCheckBox enabledForCaseBox; + // End of variables declaration//GEN-END:variables +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaOptionsPanelController.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaOptionsPanelController.java new file mode 100644 index 0000000000000000000000000000000000000000..849ea034c9989dbdddd3ead0e9e4c0d19e47996d --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaOptionsPanelController.java @@ -0,0 +1,107 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import javax.swing.JComponent; +import javax.swing.SwingUtilities; +import org.netbeans.spi.options.OptionsPanelController; +import org.openide.util.HelpCtx; +import org.openide.util.Lookup; + +@OptionsPanelController.TopLevelRegistration( + categoryName = "#OptionsCategory_Name_Options", + iconBase = "org/sleuthkit/autopsy/imageanalyzer/images/lightbulb.png", + keywords = "#OptionsCategory_Keywords_Options", + keywordsCategory = "Options" +) +@org.openide.util.NbBundle.Messages({"OptionsCategory_Name_Options=Image / Video Analzyer", "OptionsCategory_Keywords_Options=image video analyzer category "}) +public final class EurekaOptionsPanelController extends OptionsPanelController { + + private EurekaOptionsPanel panel; + + private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); + + private boolean changed; + + @Override + public void update() { + getPanel().load(); + changed = false; + } + + @Override + public void applyChanges() { + SwingUtilities.invokeLater(() -> { + getPanel().store(); + changed = false; + }); + } + + @Override + public void cancel() { + // need not do anything special, if no changes have been persisted yet + } + + @Override + public boolean isValid() { + return getPanel().valid(); + } + + @Override + public boolean isChanged() { + return changed; + } + + @Override + public HelpCtx getHelpCtx() { + return null; // new HelpCtx("...ID") if you have a help set + } + + @Override + public JComponent getComponent(Lookup masterLookup) { + return getPanel(); + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener l) { + pcs.addPropertyChangeListener(l); + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener l) { + pcs.removePropertyChangeListener(l); + } + + private EurekaOptionsPanel getPanel() { + if (panel == null) { + panel = new EurekaOptionsPanel(this); + } + return panel; + } + + void changed() { + if (!changed) { + changed = true; + pcs.firePropertyChange(OptionsPanelController.PROP_CHANGED, false, true); + } + pcs.firePropertyChange(OptionsPanelController.PROP_VALID, null, null); + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaPreferences.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaPreferences.java new file mode 100644 index 0000000000000000000000000000000000000000..6c3525b78f8b4f6fbf114ba059d882cbe056c063 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaPreferences.java @@ -0,0 +1,50 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer; + +import java.util.prefs.PreferenceChangeListener; +import java.util.prefs.Preferences; +import org.openide.util.NbPreferences; + +/** + * + */ +public class EurekaPreferences { + + private static final Preferences preferences = NbPreferences.forModule(EurekaPreferences.class); + + static final String ENABLED_BY_DEFAULT = "enabled_by_default"; + + static boolean isEnabledByDefault() { + final boolean aBoolean = preferences.getBoolean(ENABLED_BY_DEFAULT, true); + return aBoolean; + } + + static void setEnabledByDefault(boolean b) { + preferences.putBoolean(ENABLED_BY_DEFAULT, b); + } + + static void addChangeListener(PreferenceChangeListener l) { + preferences.addPreferenceChangeListener(l); + } + + static void removeChangeListener(PreferenceChangeListener l) { + preferences.removePreferenceChangeListener(l); + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaTopComponent.form b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaTopComponent.form new file mode 100644 index 0000000000000000000000000000000000000000..cf17948941e39dcf077b382efeb030fa88f4007f --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaTopComponent.form @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.4" maxVersion="1.8" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + </AuxValues> + + <Layout> + <DimensionLayout dim="0"> + <Group type="103" groupAlignment="0" attributes="0"> + <Component id="eurekaJFXPanel" pref="532" max="32767" attributes="0"/> + <Component id="listView1" min="-2" max="-2" attributes="0"/> + </Group> + </DimensionLayout> + <DimensionLayout dim="1"> + <Group type="103" groupAlignment="0" attributes="0"> + <Component id="eurekaJFXPanel" pref="389" max="32767" attributes="0"/> + <Component id="listView1" min="-2" max="-2" attributes="0"/> + </Group> + </DimensionLayout> + </Layout> + <SubComponents> + <Component class="org.openide.explorer.view.ListView" name="listView1"> + <Properties> + <Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[0, 0]"/> + </Property> + <Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[0, 0]"/> + </Property> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[0, 0]"/> + </Property> + </Properties> + </Component> + <Container class="javafx.embed.swing.JFXPanel" name="eurekaJFXPanel"> + <AuxValues> + <AuxValue name="JavaCodeGenerator_CreateCodeCustom" type="java.lang.String" value="new EurekaPanel()"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignAbsoluteLayout"> + <Property name="useNullLayout" type="boolean" value="true"/> + </Layout> + </Container> + </SubComponents> +</Form> diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaTopComponent.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaTopComponent.java new file mode 100644 index 0000000000000000000000000000000000000000..6acfe5bd80cd2056dcb207d81ed43a32d38fd757 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/EurekaTopComponent.java @@ -0,0 +1,133 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer; + +import org.netbeans.api.settings.ConvertAsProperties; +import org.openide.explorer.ExplorerManager; +import org.openide.explorer.ExplorerUtils; +import org.openide.util.Lookup; +import org.openide.util.NbBundle.Messages; +import org.openide.windows.TopComponent; +import org.sleuthkit.autopsy.imageanalyzer.gui.EurekaPanel; + +/** + * Top component which displays eureka interface. + */ +@ConvertAsProperties( + dtd = "-//org.sleuthkit.autopsy.imageanalyzer//Eureka//EN", + autostore = false) +@TopComponent.Description( + preferredID = "EurekaTopComponent", + //iconBase = "org/sleuthkit/autopsy/imageanalyzer/images/lightbulb.png", + persistenceType = TopComponent.PERSISTENCE_NEVER) +@TopComponent.Registration(mode = "timeline", openAtStartup = false) +@Messages({ + "CTL_EurekaAction=Image/Video Analysis", + "CTL_EurekaTopComponent=Image/Video Analysis", + "HINT_EurekaTopComponent=This is a Image/Video Analysis window" +}) +public final class EurekaTopComponent extends TopComponent implements ExplorerManager.Provider, Lookup.Provider { + + public final static String PREFERRED_ID = "EurekaTopComponent"; + + private final ExplorerManager em = new ExplorerManager(); + + private final Lookup lookup; + + public EurekaTopComponent() { + + setName(Bundle.CTL_EurekaTopComponent()); + setToolTipText(Bundle.HINT_EurekaTopComponent()); + + // ...and initialization of lookup variable + lookup = (ExplorerUtils.createLookup(em, getActionMap())); + initComponents(); + + ((EurekaPanel) eurekaJFXPanel).initScene(); + } + + public EurekaPanel getEurekaJFXPanel() { + return (EurekaPanel) eurekaJFXPanel; + } + + /** + * 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. + */ + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + + listView1 = new org.openide.explorer.view.ListView(); + eurekaJFXPanel = new EurekaPanel(); + + listView1.setMaximumSize(new java.awt.Dimension(0, 0)); + listView1.setMinimumSize(new java.awt.Dimension(0, 0)); + listView1.setPreferredSize(new java.awt.Dimension(0, 0)); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(eurekaJFXPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 532, Short.MAX_VALUE) + .addComponent(listView1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(eurekaJFXPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 389, Short.MAX_VALUE) + .addComponent(listView1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + ); + }// </editor-fold>//GEN-END:initComponents + // Variables declaration - do not modify//GEN-BEGIN:variables + private javafx.embed.swing.JFXPanel eurekaJFXPanel; + private org.openide.explorer.view.ListView listView1; + // End of variables declaration//GEN-END:variables + + @Override + public void componentOpened() { + + } + + @Override + public void componentClosed() { + //TODO: we could do some cleanup here + } + + void writeProperties(java.util.Properties p) { + // better to version settings since initial version as advocated at + // http://wiki.apidesign.org/wiki/PropertyFiles + p.setProperty("version", "1.0"); + // TODO store your settings + } + + void readProperties(java.util.Properties p) { + String version = p.getProperty("version"); + // TODO read your settings according to their version + } + + @Override + public ExplorerManager getExplorerManager() { + return em; + } + + @Override + public Lookup getLookup() { + return lookup; + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/FXMLConstructor.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/FXMLConstructor.java new file mode 100644 index 0000000000000000000000000000000000000000..1d4cbc39d321c7c2a6bc90f0863dd20695b8febd --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/FXMLConstructor.java @@ -0,0 +1,141 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer; + +import java.io.IOException; +import java.net.URL; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; +import org.openide.util.Exceptions; + +/** + * This class support both programmer productivity by abstracting frequently + * used code to load FXML-defined GUI components, and code performance by + * implementing a caching FXMLLoader as described at + * http://stackoverflow.com/questions/11734885/javafx2-very-poor-performance-when-adding-custom-made-fxmlpanels-to-gridpane. + */ +public class FXMLConstructor { + + private static final CachingClassLoader CACHING_CLASS_LOADER = new CachingClassLoader((FXMLLoader.getDefaultClassLoader())); + + static public void construct(Node n, String fxmlFileName) { + FXMLLoader fxmlLoader = new FXMLLoader(n.getClass().getResource(fxmlFileName)); + fxmlLoader.setRoot(n); + fxmlLoader.setController(n); + fxmlLoader.setClassLoader(CACHING_CLASS_LOADER); + + try { + fxmlLoader.load(); + } catch (IOException exception) { + try { + fxmlLoader.setClassLoader(FXMLLoader.getDefaultClassLoader()); + fxmlLoader.load(); + } catch (IOException ex) { + Exceptions.printStackTrace(ex); + } + } + } + + /** + * The default FXMLLoader does not cache information about previously loaded + * FXML files. See + * http://stackoverflow.com/questions/11734885/javafx2-very-poor-performance-when-adding-custom-made-fxmlpanels-to-gridpane. + * for more details. As a partial workaround, we cache information on + * previously loaded classes. This does not solve all performance issues, + * but is a big improvement. + */ + static public class CachingClassLoader extends ClassLoader { + + private final Map<String, Class> classes = new HashMap<String, Class>(); + + private final ClassLoader parent; + + public CachingClassLoader(ClassLoader parent) { + this.parent = parent; + } + + @Override + public Class<?> loadClass(String name) throws ClassNotFoundException { + Class<?> c = findClass(name); + if (c == null) { + throw new ClassNotFoundException(name); + } + return c; + } + + @Override + protected Class<?> findClass(String className) throws ClassNotFoundException { +// System.out.print("try to load " + className); + if (classes.containsKey(className)) { + Class<?> result = classes.get(className); + return result; + } else { + try { + Class<?> result = parent.loadClass(className); +// System.out.println(" -> success!"); + classes.put(className, result); + return result; + } catch (ClassNotFoundException ignore) { +// System.out.println(); + classes.put(className, null); + return null; + } + } + } + + // ========= delegating methods ============= + @Override + public URL getResource(String name) { + return parent.getResource(name); + } + + @Override + public Enumeration<URL> getResources(String name) throws IOException { + return parent.getResources(name); + } + + @Override + public String toString() { + return parent.toString(); + } + + @Override + public void setDefaultAssertionStatus(boolean enabled) { + parent.setDefaultAssertionStatus(enabled); + } + + @Override + public void setPackageAssertionStatus(String packageName, boolean enabled) { + parent.setPackageAssertionStatus(packageName, enabled); + } + + @Override + public void setClassAssertionStatus(String className, boolean enabled) { + parent.setClassAssertionStatus(className, enabled); + } + + @Override + public void clearAssertionStatus() { + parent.clearAssertionStatus(); + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/FileIDSelectionModel.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/FileIDSelectionModel.java new file mode 100644 index 0000000000000000000000000000000000000000..c7e2e2a0900a14facb960c26aa4da89d9fb9809e --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/FileIDSelectionModel.java @@ -0,0 +1,113 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-14 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer; + +import java.util.Arrays; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.ObservableSet; +import org.sleuthkit.autopsy.coreutils.Logger; + +/** Singleton that manages set of fileIds, as well as + * last selected fileID. + * NOTE: When we had synchronization on selected and lastSelectedProp we got + * deadlocks with the tiles during selection + */ +public class FileIDSelectionModel { + + private static final Logger LOGGER = Logger.getLogger(FileIDSelectionModel.class.getName()); + + private static FileIDSelectionModel instance; + + private final ObservableSet<Long> selected = FXCollections.observableSet(); + + private final ReadOnlyObjectWrapper<Long> lastSelectedProp = new ReadOnlyObjectWrapper<>(); + + public static synchronized FileIDSelectionModel getInstance() { + if (instance == null) { + instance = new FileIDSelectionModel(); + } + return instance; + } + + public FileIDSelectionModel() { + super(); + } + + public void toggleSelection(Long id) { + boolean contained = selected.contains(id); + + if (contained) { + selected.remove(id); + setLastSelected(null); + } else { + selected.add(id); + setLastSelected(id); + } + } + + public void clearAndSelectAll(Long... ids) { + selected.clear(); + selected.addAll(Arrays.asList(ids)); + setLastSelected(ids[ids.length - 1]); + } + + public void clearAndSelect(Long id) { + selected.clear(); + selected.add(id); + setLastSelected(id); + } + + public void select(Long id) { + selected.add(id); + setLastSelected(id); + } + + public void deSelect(Long id) { + selected.remove(id); + setLastSelected(null); + } + + public void clearSelection() { + selected.clear(); + setLastSelected(null); + } + + public boolean isSelected(Long id) { + return selected.contains(id); + } + + public ReadOnlyObjectProperty<Long> lastSelectedProperty() { + return lastSelectedProp.getReadOnlyProperty(); + } + + private void setLastSelected(Long id) { + lastSelectedProp.set(id); + } + + public ObservableSet<Long> getSelected() { + return selected; + } + + public void clearAndSelectAll(ObservableList<Long> ids) { + clearAndSelectAll(ids.toArray(new Long[ids.size()])); + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/FileUpdateEvent.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/FileUpdateEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..3703a380ee3e65377d00fd14c9b7873e108e364f --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/FileUpdateEvent.java @@ -0,0 +1,70 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer; + +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import javax.annotation.concurrent.Immutable; + +@Immutable +public class FileUpdateEvent { + + private final Set<Long> updatedFiles; + + private final DrawableAttribute changedAttribute; + + private final UpdateType updateType; + + public UpdateType getUpdateType() { + return updateType; + } + + public Collection<Long> getUpdatedFiles() { + return updatedFiles; + } + + public DrawableAttribute getChangedAttribute() { + return changedAttribute; + } + + public FileUpdateEvent(Collection<? extends Long> updatedFiles, DrawableAttribute changedAttribute, UpdateType updateType) { + this.updatedFiles = new HashSet<>(updatedFiles); + this.changedAttribute = changedAttribute; + this.updateType = updateType; + } + + public FileUpdateEvent(Collection<? extends Long> updatedFiles, DrawableAttribute changedAttribute) { + this.updatedFiles = new HashSet<>(updatedFiles); + this.changedAttribute = changedAttribute; + this.updateType = UpdateType.FILE_UPDATED; + } + + public FileUpdateEvent(Collection<? extends Long> updatedFiles) { + this.updatedFiles = new HashSet<>(updatedFiles); + changedAttribute = null; + this.updateType = UpdateType.FILE_UPDATED; + } + + static public enum UpdateType { + + FILE_UPDATED, FILE_REMOVED; + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/FileUpdateListener.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/FileUpdateListener.java new file mode 100644 index 0000000000000000000000000000000000000000..f0fced781b46e96f7307ca50d97c9d76ddb2badc --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/FileUpdateListener.java @@ -0,0 +1,29 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer; + +import java.util.EventListener; + +/** + * + */ +public interface FileUpdateListener extends EventListener { + + public void handleFileUpdate(FileUpdateEvent evt); +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/IconCache.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/IconCache.java new file mode 100644 index 0000000000000000000000000000000000000000..6b192441370e405be9ad8b58c73a1690c1404262 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/IconCache.java @@ -0,0 +1,207 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer; + +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.embed.swing.SwingFXUtils; +import javafx.scene.image.Image; +import javafx.scene.image.WritableImage; +import javax.imageio.IIOException; +import javax.imageio.ImageIO; +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.openide.util.Exceptions; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.corelibs.ScalrWrapper; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.ReadContentInputStream; +import org.sleuthkit.datamodel.TskCoreException; + +/** Manages creation and access of icons. Keeps a cache in memory of most + * recently used icons, and a disk cache of all icons. */ +public class IconCache { + + static private IconCache instance; + + private static final int MAX_ICON_SIZE = 300; + + private static final Logger LOGGER = Logger.getLogger(IconCache.class.getName()); + + private static final Cache<Long, Optional<Image>> cache = CacheBuilder.newBuilder().maximumSize(1000).softValues().expireAfterAccess(10, TimeUnit.MINUTES).build(); + + public SimpleIntegerProperty iconSize = new SimpleIntegerProperty(200); + + private final Executor imageSaver = Executors.newSingleThreadExecutor(new BasicThreadFactory.Builder().namingPattern("icon saver-%d").build()); + + private IconCache() { + + } + + synchronized static public IconCache getDefault() { + if (instance == null) { + instance = new IconCache(); + } + return instance; + } + + public Image get(DrawableFile file) { + try { + return cache.get(file.getId(), () -> load(file)).orElse(null); + } catch (CacheLoader.InvalidCacheLoadException | ExecutionException ex) { + LOGGER.log(Level.WARNING, "failed to load icon for file: " + file.getName(), ex); + return null; + } + } + + public Image get(Long fileID) { + try { + return get(EurekaController.getDefault().getFileFromId(fileID)); + } catch (TskCoreException ex) { + Exceptions.printStackTrace(ex); + return null; + } + } + + public Optional<Image> load(DrawableFile file) throws IIOException { + + Image icon = null; + File cacheFile; + try { + cacheFile = getCacheFile(file.getId()); + } catch (IllegalStateException e) { + LOGGER.log(Level.WARNING, "can't load icon when no case is open"); + return Optional.empty(); + } + + // If a thumbnail file is already saved locally + if (cacheFile.exists()) { + try { + int dim = iconSize.get(); + icon = new Image(cacheFile.toURI().toURL().toString(), dim, dim, true, false, true); + } catch (MalformedURLException ex) { + Exceptions.printStackTrace(ex); + } + } + + if (icon == null) { + // Logger.getAnonymousLogger().warning("wrong size cache found for image " + getName()); + icon = generateAndSaveIcon(file); + } + return Optional.ofNullable(icon); + + } + + private static File getCacheFile(long id) { + return new File(Case.getCurrentCase().getCacheDirectory() + File.separator + id + ".png"); + } + + private Image generateAndSaveIcon(final DrawableFile file) { + Image img; + //TODO: should we wrap this in a BufferedInputStream? -jm + try (ReadContentInputStream inputStream = new ReadContentInputStream(file.getAbstractFile())) { + img = new Image(inputStream, MAX_ICON_SIZE, MAX_ICON_SIZE, true, true); + if (img.isError()) { + LOGGER.log(Level.WARNING, "problem loading image: {0}. {1}", new Object[]{file.getName(), img.getException().getLocalizedMessage()}); + return fallbackToSwingImage(file); + } else { + imageSaver.execute(() -> { + saveIcon(file, img); + }); + } + } catch (IOException ex) { + return fallbackToSwingImage(file); + } + + return img; + + } + /* Generate a scaled image */ + + private Image fallbackToSwingImage(final DrawableFile file) { + final BufferedImage generateSwingIcon = generateSwingIcon(file); + if (generateSwingIcon != null) { + WritableImage toFXImage = SwingFXUtils.toFXImage(generateSwingIcon, null); + if (toFXImage != null) { + imageSaver.execute(() -> { + saveIcon(file, toFXImage); + }); + } + + return toFXImage; + } else { + return null; + } + } + + private BufferedImage generateSwingIcon(DrawableFile file) { + try (ReadContentInputStream inputStream = new ReadContentInputStream(file.getAbstractFile())) { + BufferedImage bi = ImageIO.read(inputStream); + if (bi == null) { + LOGGER.log(Level.WARNING, "No image reader for file: {0}", file.getName()); + return null; + } else { + try { + if (Math.max(bi.getWidth(), bi.getHeight()) > MAX_ICON_SIZE) { + bi = ScalrWrapper.resizeFast(bi, iconSize.get()); + } + } catch (IllegalArgumentException e) { + LOGGER.log(Level.WARNING, "scalr could not scale image to 0: {0}", file.getName()); + } catch (OutOfMemoryError e) { + LOGGER.log(Level.WARNING, "scalr could not scale image (too large): {0}", file.getName()); + return null; + } + } + return bi; + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "Could not read image: " + file.getName(), ex); + return null; + } + } + + private void saveIcon(final DrawableFile file, final Image bi) { + try { + /* save the icon in a background thread. profiling + * showed that it can take as much time as making + * the icon? -bc + * + * We don't do this now as it doesn't fit the + * current model of ui-related backgroiund tasks, + * and there might be complications to not just + * blocking (eg having more than one task to + * create the same icon -jm */ + File f = getCacheFile(file.getId()); + ImageIO.write(SwingFXUtils.fromFXImage(bi, null), "png", f); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "failed to save generated icon ", ex); + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/LoggedTask.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/LoggedTask.java new file mode 100644 index 0000000000000000000000000000000000000000..58847bf63dc3af264b275b21f9cb098845fd40ad --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/LoggedTask.java @@ -0,0 +1,75 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-14 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer; + +import org.sleuthkit.autopsy.imageanalyzer.progress.ProgressAdapter; +import java.util.logging.Level; +import javafx.concurrent.Task; +import org.openide.util.Cancellable; +import org.sleuthkit.autopsy.coreutils.Logger; + +/** + * + */ +public abstract class LoggedTask<T> extends Task<T> implements Cancellable, ProgressAdapter { + + private static final Logger LOGGER = Logger.getLogger(LoggedTask.class.getName()); + private final String taskName; + private final Boolean logStateChanges; + + public String getTaskName() { + return taskName; + } + + public LoggedTask(String taskName, Boolean logStateChanges) { + this.taskName = taskName; + this.logStateChanges = logStateChanges; + } + + @Override + protected void cancelled() { + super.cancelled(); + if (logStateChanges) { + LOGGER.log(Level.WARNING, "{0} cancelled!", taskName); + } + } + + @Override + protected void failed() { + super.failed(); + LOGGER.log(Level.SEVERE, taskName + " failed", getException()); + + } + + @Override + protected void scheduled() { + super.scheduled(); + if (logStateChanges) { + LOGGER.log(Level.INFO, "{0} scheduled", taskName); + } + } + + @Override + protected void succeeded() { + super.succeeded(); + if (logStateChanges) { + LOGGER.log(Level.INFO, "{0} succeeded", taskName); + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/ObservableStack.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/ObservableStack.java new file mode 100644 index 0000000000000000000000000000000000000000..096fa54b91ec93141f9c39ed4f86512a1c3c4a78 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/ObservableStack.java @@ -0,0 +1,59 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer; + +import javafx.beans.property.SimpleListProperty; +import javafx.collections.FXCollections; + +/** + * + */ +public class ObservableStack<T> extends SimpleListProperty<T> { + + public ObservableStack() { + super(FXCollections.synchronizedObservableList(FXCollections.observableArrayList())); + } + + public void push(T item) { + synchronized (this) { + add(0, item); + } + } + + public T pop() { + synchronized (this) { + if (isEmpty()) { + return null; + } else { + return remove(0); + } + } + } + + public T peek() { + synchronized (this) { + if (isEmpty()) { + return null; + } else { + return get(0); + } + } + } + +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/OnStart.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/OnStart.java new file mode 100644 index 0000000000000000000000000000000000000000..7234fa1b9509d701a005f76d19a9ddb7574e8746 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/OnStart.java @@ -0,0 +1,55 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer; + +import javafx.application.Platform; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.ingest.IngestManager; + +/** + * + * The {@link OnStart} annotation tells NetBeans to invoke this class's + * {@link EurekaModule#run()} method + */ +@org.openide.modules.OnStart +public class OnStart implements Runnable { + + static private final Logger LOGGER = Logger.getLogger(OnStart.class.getName()); + + /** + * make sure that the eureka listeners get setup as early as possible, and + * do other setup stuff. + * + * This method is invoked by virtue of the {@link OnStart} annotation on the + * {@link EurekaModule} class + */ + @Override + public void run() { + Platform.setImplicitExit(false); + + LOGGER.info("setting up eureka listeners"); + + IngestManager.getInstance().addIngestJobEventListener(AutopsyListener.getDefault().getIngestJobEventListener()); + IngestManager.getInstance().addIngestModuleEventListener(AutopsyListener.getDefault().getIngestModuleEventListener()); + + Case.addPropertyChangeListener(AutopsyListener.getDefault().getCaseListener()); + } + +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/PerCaseProperties.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/PerCaseProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..6f8b2c89e7633f7d92f683833bb71624f0810405 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/PerCaseProperties.java @@ -0,0 +1,297 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.logging.Level; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; + +/** + * + */ +public class PerCaseProperties { + + public static final String ENABLED = "enabled"; + + public static final String STALE = "stale"; + + private final Case c; + + /** the constructor */ + PerCaseProperties(Case c) { + this.c = c; + } + + /** + * Makes a new config file of the specified name. Do not include the + * extension. + * + * @param moduleName - The name of the config file to make + * + * @return True if successfully created, false if already exists or an error + * is thrown. + */ + public synchronized boolean makeConfigFile(String moduleName) { + if (!configExists(moduleName)) { + File propPath = new File(getPropertyPath(moduleName)); + File parent = new File(propPath.getParent()); + if (!parent.exists()) { + parent.mkdirs(); + } + Properties props = new Properties(); + try { + propPath.createNewFile(); + try (FileOutputStream fos = new FileOutputStream(propPath)) { + props.store(fos, ""); + } + } catch (IOException e) { + Logger.getLogger(PerCaseProperties.class.getName()).log(Level.WARNING, "Was not able to create a new properties file.", e); //NON-NLS + return false; + } + return true; + } + return false; + } + + /** + * Determines if a given properties file exists or not. + * + * @param moduleName - The name of the config file to evaluate + * + * @return true if the config exists, false otherwise. + */ + public synchronized boolean configExists(String moduleName) { + File f = new File(c.getCaseDirectory() + File.separator + moduleName + ".properties"); + return f.exists(); + } + + public synchronized boolean settingExists(String moduleName, String settingName) { + if (!configExists(moduleName)) { + return false; + } + try { + Properties props = fetchProperties(moduleName); + return (props.getProperty(settingName) != null); + } catch (IOException e) { + return false; + } + + } + + /** + * Returns the path of the given properties file. + * + * @param moduleName - The name of the config file to evaluate + * + * @return The path of the given config file. Returns null if the config + * file doesn't exist. + */ + private synchronized String getPropertyPath(String moduleName) { + return c.getCaseDirectory() + File.separator + moduleName + ".properties"; //NON-NLS + } + + /** + * Returns the given properties file's setting as specific by settingName. + * + * @param moduleName - The name of the config file to read from. + * @param settingName - The setting name to retrieve. + * + * @return - the value associated with the setting. + * + * @throws IOException + */ + public synchronized String getConfigSetting(String moduleName, String settingName) { + if (!configExists(moduleName)) { + makeConfigFile(moduleName); + Logger.getLogger(PerCaseProperties.class.getName()).log(Level.INFO, "File did not exist. Created file [" + moduleName + ".properties]"); //NON-NLS NON-NLS + } + + try { + Properties props = fetchProperties(moduleName); + + return props.getProperty(settingName); + } catch (IOException e) { + Logger.getLogger(PerCaseProperties.class.getName()).log(Level.WARNING, "Could not read config file [" + moduleName + "]", e); //NON-NLS + return null; + } + + } + + /** + * Returns the given properties file's map of settings. + * + * @param moduleName - the name of the config file to read from. + * + * @return - the map of all key:value pairs representing the settings of the + * config. + * + * @throws IOException + */ + public synchronized Map< String, String> getConfigSettings(String moduleName) { + + if (!configExists(moduleName)) { + makeConfigFile(moduleName); + Logger.getLogger(PerCaseProperties.class.getName()).log(Level.INFO, "File did not exist. Created file [" + moduleName + ".properties]"); //NON-NLS NON-NLS + } + try { + Properties props = fetchProperties(moduleName); + + Set<String> keys = props.stringPropertyNames(); + Map<String, String> map = new HashMap<>(); + + for (String s : keys) { + map.put(s, props.getProperty(s)); + } + + return map; + } catch (IOException e) { + Logger.getLogger(PerCaseProperties.class.getName()).log(Level.WARNING, "Could not read config file [" + moduleName + "]", e); //NON-NLS + return null; + } + } + + /** + * Sets the given properties file to the given setting map. + * + * @param moduleName - The name of the module to be written to. + * @param settings - The mapping of all key:value pairs of settings to add + * to the config. + */ + public synchronized void setConfigSettings(String moduleName, Map<String, String> settings) { + if (!configExists(moduleName)) { + makeConfigFile(moduleName); + Logger.getLogger(PerCaseProperties.class.getName()).log(Level.INFO, "File did not exist. Created file [" + moduleName + ".properties]"); //NON-NLS NON-NLS + } + try { + Properties props = fetchProperties(moduleName); + + for (Map.Entry<String, String> kvp : settings.entrySet()) { + props.setProperty(kvp.getKey(), kvp.getValue()); + } + + File path = new File(getPropertyPath(moduleName)); + try (FileOutputStream fos = new FileOutputStream(path)) { + props.store(fos, "Changed config settings(batch)"); //NON-NLS + } + } catch (IOException e) { + Logger.getLogger(PerCaseProperties.class.getName()).log(Level.WARNING, "Property file exists for [" + moduleName + "] at [" + getPropertyPath(moduleName) + "] but could not be loaded.", e); //NON-NLS NON-NLS NON-NLS + } + } + + /** + * Sets the given properties file to the given settings. + * + * @param moduleName - The name of the module to be written to. + * @param settingName - The name of the setting to be modified. + * @param settingVal - the value to set the setting to. + */ + public synchronized void setConfigSetting(String moduleName, String settingName, String settingVal) { + if (!configExists(moduleName)) { + makeConfigFile(moduleName); + Logger.getLogger(PerCaseProperties.class.getName()).log(Level.INFO, "File did not exist. Created file [" + moduleName + ".properties]"); //NON-NLS NON-NLS + } + + try { + Properties props = fetchProperties(moduleName); + + props.setProperty(settingName, settingVal); + + File path = new File(getPropertyPath(moduleName)); + try (FileOutputStream fos = new FileOutputStream(path)) { + props.store(fos, "Changed config settings(single)"); //NON-NLS + } + } catch (IOException e) { + Logger.getLogger(PerCaseProperties.class.getName()).log(Level.WARNING, "Property file exists for [" + moduleName + "] at [" + getPropertyPath(moduleName) + "] but could not be loaded.", e); //NON-NLS NON-NLS NON-NLS + } + } + + /** + * Removes the given key from the given properties file. + * + * @param moduleName - The name of the properties file to be modified. + * @param key - the name of the key to remove. + */ + public synchronized void removeProperty(String moduleName, String key) { + if (!configExists(moduleName)) { + makeConfigFile(moduleName); + Logger.getLogger(PerCaseProperties.class.getName()).log(Level.INFO, "File did not exist. Created file [" + moduleName + ".properties]"); //NON-NLS NON-NLS + } + + try { + if (getConfigSetting(moduleName, key) != null) { + Properties props = fetchProperties(moduleName); + + props.remove(key); + File path = new File(getPropertyPath(moduleName)); + try (FileOutputStream fos = new FileOutputStream(path)) { + props.store(fos, "Removed " + key); //NON-NLS + } + } + } catch (IOException e) { + Logger.getLogger(PerCaseProperties.class.getName()).log(Level.WARNING, "Could not remove property from file, file not found", e); //NON-NLS + } + } + + /** + * Returns the properties file as specified by moduleName. + * + * @param moduleName + * + * @return Properties file as specified by moduleName. + * + * @throws IOException + */ + private Properties fetchProperties(String moduleName) throws IOException { + if (!configExists(moduleName)) { + makeConfigFile(moduleName); + Logger.getLogger(PerCaseProperties.class.getName()).log(Level.INFO, "File did not exist. Created file [" + moduleName + ".properties]"); //NON-NLS NON-NLS + } + Properties props; + try (InputStream inputStream = new FileInputStream(getPropertyPath(moduleName))) { + props = new Properties(); + props.load(inputStream); + } + return props; + } + +// /** +// * Gets the property file as specified. +// * +// * @param moduleName +// * +// * @return A new file handle, returns null if the file does not exist. +// */ +// public File getPropertyFile(String moduleName) { +// String path = getPropertyPath(moduleName); +// if (path == null) { +// return null; +// } else { +// return new File(getPropertyPath(moduleName)); +// } +// } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/TagUtils.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/TagUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..362af325685d87afe4c23f6288533c9033a562f3 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/TagUtils.java @@ -0,0 +1,156 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.logging.Level; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.scene.control.MenuItem; +import javafx.scene.control.SplitMenuButton; +import javafx.scene.image.ImageView; +import org.apache.commons.lang3.StringUtils; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.services.TagsManager; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.imageanalyzer.actions.AddDrawableTagAction; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.Category; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute; +import org.sleuthkit.datamodel.TagName; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Contains static methods for dealing with Tags in Eureka + */ +public class TagUtils { + + private static final String follow_Up = "Follow Up"; + + private static TagName followUpTagName; + + private final static List<TagListener> listeners = new ArrayList<>(); + + synchronized public static TagName getFollowUpTagName() throws TskCoreException { + if (followUpTagName == null) { + followUpTagName = getTagName(follow_Up); + } + return followUpTagName; + } + + static public Collection<TagName> getNonCategoryTagNames() { + List<TagName> nonCatTagNames = new ArrayList<>(); + List<TagName> allTagNames; + try { + allTagNames = Case.getCurrentCase().getServices().getTagsManager().getAllTagNames(); + for (TagName tn : allTagNames) { + if (tn.getDisplayName().startsWith(Category.CATEGORY_PREFIX) == false) { + nonCatTagNames.add(tn); + } + } + } catch (TskCoreException | IllegalStateException ex) { + Logger.getLogger(TagUtils.class.getName()).log(Level.WARNING, "couldn't access case", ex); + } + + return nonCatTagNames; + } + + synchronized static public TagName getTagName(String displayName) throws TskCoreException { + try { + final TagsManager tagsManager = Case.getCurrentCase().getServices().getTagsManager(); + + for (TagName tn : tagsManager.getAllTagNames()) { + if (displayName.equals(tn.getDisplayName())) { + return tn; + } + } + try { + return tagsManager.addTagName(displayName); + } catch (TagsManager.TagNameAlreadyExistsException ex) { + throw new TskCoreException("tagame exists but wasn't found", ex); + } + } catch (IllegalStateException ex) { + Logger.getLogger(TagUtils.class.getName()).log(Level.SEVERE, "Case was closed out from underneath", ex); + throw new TskCoreException("Case was closed out from underneath", ex); + } + } + + /** + * convert a collection of TagNames to a 'pretty' string by concatenating + * their displaynames separated by a semi-colons + * + * Collection<TagName> => "tagname1, tagname2, tagname3" + * + * @param tagNames + * + * @return + */ + static public String collectionToString(Collection<TagName> tagNames) { + ArrayList<Object> stringNames = new ArrayList<>(); + for (TagName tn : tagNames) { + stringNames.add(tn.getDisplayName()); + } + final String join = StringUtils.join(stringNames, "; "); + return join; + } + + public static void fireChange(Collection<Long> ids) { + synchronized (listeners) { + for (TagListener list : listeners) { + list.handleTagsChanged(ids); + } + } + } + + public static void registerListener(TagListener aThis) { + synchronized (listeners) { + listeners.add(aThis); + } + } + + public static void unregisterListener(TagListener aThis) { + synchronized (listeners) { + listeners.remove(aThis); + } + } + + /** + * + * @param tn the value of tn + * @param eurekaToolbar the value of eurekaToolbar + */ + static public MenuItem createSelTagMenuItem(final TagName tn, final SplitMenuButton tagSelectedMenuButton) { + final MenuItem menuItem = new MenuItem(tn.getDisplayName(), new ImageView(DrawableAttribute.TAGS.getIcon())); + menuItem.setOnAction(new EventHandler<ActionEvent>() { + @Override + public void handle(ActionEvent t) { + AddDrawableTagAction.getInstance().addTag(tn, ""); + tagSelectedMenuButton.setText(tn.getDisplayName()); + tagSelectedMenuButton.setOnAction(this); + } + }); + return menuItem; + } + + public static interface TagListener { + + public void handleTagsChanged(Collection<Long> ids); + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/ThreadConfined.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/ThreadConfined.java new file mode 100644 index 0000000000000000000000000000000000000000..2e33930be94ebcdbd69096e5173eee3f486b3e74 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/ThreadConfined.java @@ -0,0 +1,22 @@ +package org.sleuthkit.autopsy.imageanalyzer; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.FIELD}) +@Inherited +@Documented +public @interface ThreadConfined { + + ThreadType type(); + + public enum ThreadType { + + ANY, UI, NOT_UI + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/ThreadUtils.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/ThreadUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..e1a34571e842456a7650f40efe3522c5bb938c2b --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/ThreadUtils.java @@ -0,0 +1,92 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer; + +import java.lang.reflect.InvocationTargetException; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.atomic.AtomicReference; +import javafx.application.Platform; +import javax.swing.SwingUtilities; + +/** + * + */ +public class ThreadUtils { + + /** + * analogous to {@link SwingUtilities#invokeAndWait(java.lang.Runnable) with a return value. + * Should be used extremely cautiausly as it blocks the edt thread. + * + * @param <E> The type of the return value generated by the given Callable, + * r. + * @param r A callable to be invoked on the ui thread. + * + * @return the result of the Callable's call method; + */ + public static <E> E invokeAndWait(final Callable<E> r) { + final AtomicReference<E> ref = new AtomicReference<>(); + final AtomicReference<Exception> except = new AtomicReference<>(); + try { + SwingUtilities.invokeAndWait(() -> { + try { + ref.set(r.call()); + } catch (Exception e) { + except.set(e); + } + }); + } catch (InterruptedException | InvocationTargetException ex) { + throw new RuntimeException(ex); + } finally { + if (except.get() != null) { + throw new RuntimeException(except.get()); + } else { + return ref.get(); + } + } + } + + /** + * analogous to {@link SwingUtilities#invokeAndWait(java.lang.Runnable) for JavaFX. + * Should be used extremely cautiausly as it blocks the JavaFx UI thread. + * + * @param runnable the runnable to be invoked on the javafx ui thred + * + * @throws InterruptedException + * @throws ExecutionException + */ + public static void runAndWait(Runnable runnable) throws InterruptedException, ExecutionException { + FutureTask future = new FutureTask(runnable, null); + Platform.runLater(future); + future.get(); + } + + /** + * + * @param r the value of r + */ + public static void runNowOrLater(Runnable r) { + if (Platform.isFxApplicationThread()) { + r.run(); + } else { + Platform.runLater(r); + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/AddDrawableTagAction.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/AddDrawableTagAction.java new file mode 100644 index 0000000000000000000000000000000000000000..ee39b503ddaab312a34b5d9d3c1a6f65ec815cec --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/AddDrawableTagAction.java @@ -0,0 +1,117 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.actions; + +import org.sleuthkit.autopsy.imageanalyzer.EurekaController; +import org.sleuthkit.autopsy.imageanalyzer.FileIDSelectionModel; +import org.sleuthkit.autopsy.imageanalyzer.FileUpdateEvent; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import javafx.scene.control.Menu; +import javax.swing.JOptionPane; +import javax.swing.SwingWorker; +import org.openide.util.Utilities; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.TagName; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Instances of this Action allow users to apply tags to content. + * + * //TODO: since we are not using actionsGlobalContext anymore and this has + * diverged from autopsy action, make this extend from controlsfx Action + */ +public class AddDrawableTagAction extends AddTagAction { + + private static final Logger LOGGER = Logger.getLogger(AddDrawableTagAction.class.getName()); + + // 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 AddDrawableTagAction instance; + + public static synchronized AddDrawableTagAction getInstance() { + if (null == instance) { + instance = new AddDrawableTagAction(); + } + return instance; + } + + private AddDrawableTagAction() { + } + + public Menu getPopupMenu() { + return new TagMenu(); + } + + @Override + protected String getActionDisplayName() { + return Utilities.actionsGlobalContext().lookupAll(AbstractFile.class).size() > 1 ? "Tag Files" : "Tag File"; + } + + @Override + public void addTag(TagName tagName, String comment) { + Set<Long> selectedFiles = new HashSet<>(FileIDSelectionModel.getInstance().getSelected()); + + new SwingWorker<Void, Void>() { + + @Override + protected Void doInBackground() throws Exception { + for (Long fileID : selectedFiles) { + try { + DrawableFile file = EurekaController.getDefault().getFileFromId(fileID); + LOGGER.log(Level.INFO, "tagging {0} with {1} and comment {2}", new Object[]{file.getName(), tagName.getDisplayName(), comment}); + Case.getCurrentCase().getServices().getTagsManager().addContentTag(file, tagName, comment); + } catch (IllegalStateException ex) { + LOGGER.log(Level.SEVERE, "Case was closed out from underneath Updatefile task", ex); + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "Error tagging result", ex); + JOptionPane.showMessageDialog(null, "Unable to tag " + fileID + ".", "Tagging Error", JOptionPane.ERROR_MESSAGE); + } + + //make sure rest of ui hears category change. + EurekaController.getDefault().handleFileUpdate(new FileUpdateEvent(Collections.singleton(fileID), DrawableAttribute.TAGS)); + + + } + + refreshDirectoryTree(); + return null; + } + + @Override + protected void done() { + super.done(); + try { + get(); + } catch (InterruptedException | ExecutionException ex) { + LOGGER.log(Level.SEVERE, "unexpected exception while tagging files", ex); + } + } + + }.execute(); + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/AddTagAction.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/AddTagAction.java new file mode 100644 index 0000000000000000000000000000000000000000..f832800c00e9b30f449220cd9b4b1b1e96d6366a --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/AddTagAction.java @@ -0,0 +1,158 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.actions; + +import org.sleuthkit.autopsy.imageanalyzer.datamodel.Category; +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.logging.Level; +import javafx.event.ActionEvent; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuItem; +import javax.swing.SwingUtilities; +import org.openide.util.Exceptions; +import org.sleuthkit.autopsy.actions.GetTagNameAndCommentDialog; +import org.sleuthkit.autopsy.actions.GetTagNameDialog; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.services.TagsManager; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.directorytree.DirectoryTreeTopComponent; +import org.sleuthkit.datamodel.TagName; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * An abstract base class for Actions that allow users to tag SleuthKit data + * model objects. + */ +abstract class AddTagAction { + + protected void refreshDirectoryTree() { + // The way the "directory tree" currently works, a new tags sub-tree + // needs to be made to reflect the results of invoking tag Actions. + SwingUtilities.invokeLater(() -> DirectoryTreeTopComponent.findInstance().refreshContentTreeSafe()); + + } + + protected static final String NO_COMMENT = ""; + + AddTagAction() { + } + + /** + * Template method to allow derived classes to provide a string for for a + * menu item label. + */ + abstract protected String getActionDisplayName(); + + /** + * Template method to allow derived classes to add the indicated tag and + * comment to one or more a SleuthKit data model objects. + */ + abstract protected void addTag(TagName tagName, String comment); + + /** + * Instances of this class implement a context menu user interface for + * creating or selecting a tag name for a tag and specifying an optional tag + * comment. + */ + // @@@ This user interface has some significant usability issues and needs + // to be reworked. + protected class TagMenu extends Menu { + + TagMenu() { + super(getActionDisplayName()); + + // Get the current set of tag names. + TagsManager tagsManager = Case.getCurrentCase().getServices().getTagsManager(); + List<TagName> tagNames = null; + try { + tagNames = tagsManager.getAllTagNames(); + } catch (TskCoreException ex) { + Logger.getLogger(TagsManager.class.getName()).log(Level.SEVERE, "Failed to get tag names", ex); + } + + // Create a "Quick Tag" sub-menu. + Menu quickTagMenu = new Menu("Quick Tag"); + getItems().add(quickTagMenu); + + // Each tag name in the current set of tags gets its own menu item in + // the "Quick Tags" sub-menu. Selecting one of these menu items adds + // a tag with the associated tag name. + if (null != tagNames && !tagNames.isEmpty()) { + for (final TagName tagName : tagNames) { + if (tagName.getDisplayName().startsWith(Category.CATEGORY_PREFIX) == false) { + MenuItem tagNameItem = new MenuItem(tagName.getDisplayName()); + tagNameItem.setOnAction((ActionEvent t) -> { + addTag(tagName, NO_COMMENT); + refreshDirectoryTree(); + }); + quickTagMenu.getItems().add(tagNameItem); + } + } + } else { + MenuItem empty = new MenuItem("No tags"); + empty.setDisable(true); + quickTagMenu.getItems().add(empty); + } + + // quickTagMenu.addSeparator(); + // The "Quick Tag" menu also gets an "Choose Tag..." menu item. + // Selecting this item initiates a dialog that can be used to create + // or select a tag name and adds a tag with the resulting name. + MenuItem newTagMenuItem = new MenuItem("New Tag..."); + newTagMenuItem.setOnAction((ActionEvent t) -> { + try { + SwingUtilities.invokeAndWait(() -> { + TagName tagName = GetTagNameDialog.doDialog(); + if (tagName != null) { + addTag(tagName, NO_COMMENT); + refreshDirectoryTree(); + } + }); + } catch (InterruptedException | InvocationTargetException ex) { + Exceptions.printStackTrace(ex); + } + }); + quickTagMenu.getItems().add(newTagMenuItem); + + // Create a "Choose Tag and Comment..." menu item. Selecting this item initiates + // a dialog that can be used to create or select a tag name with an + // optional comment and adds a tag with the resulting name. + MenuItem tagAndCommentItem = new MenuItem("Tag and Comment..."); + tagAndCommentItem.setOnAction((ActionEvent t) -> { + try { + SwingUtilities.invokeAndWait(() -> { + GetTagNameAndCommentDialog.TagNameAndComment tagNameAndComment = GetTagNameAndCommentDialog.doDialog(); + if (null != tagNameAndComment) { + if (tagNameAndComment.getTagName().getDisplayName().startsWith(Category.CATEGORY_PREFIX)) { + new CategorizeAction().addTag(tagNameAndComment.getTagName(), tagNameAndComment.getComment()); + } else { + AddDrawableTagAction.getInstance().addTag(tagNameAndComment.getTagName(), tagNameAndComment.getComment()); + } + refreshDirectoryTree(); + } + }); + } catch (InterruptedException | InvocationTargetException ex) { + Exceptions.printStackTrace(ex); + } + }); + getItems().add(tagAndCommentItem); + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/Back.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/Back.java new file mode 100644 index 0000000000000000000000000000000000000000..e003612fb30d18401f7f18ff2475ed43c649f554 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/Back.java @@ -0,0 +1,50 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.actions; + +import javafx.event.ActionEvent; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import org.controlsfx.control.action.AbstractAction; +import org.sleuthkit.autopsy.imageanalyzer.EurekaController; + +/** + * + */ +public class Back extends AbstractAction { + + private static final Image BACK_IMAGE = new Image("/org/sleuthkit/autopsy/imageanalyzer/images/arrow-180.png", 16, 16, true, true, true); + + private final EurekaController controller; + + public Back(EurekaController controller) { + super("Back"); + setGraphic(new ImageView(BACK_IMAGE)); + setAccelerator(new KeyCodeCombination(KeyCode.LEFT, KeyCodeCombination.ALT_DOWN)); + this.controller = controller; + disabledProperty().bind(controller.getHistoryStack().sizeProperty().isEqualTo(0)); + } + + @Override + public void handle(ActionEvent ae) { + controller.goBack(); + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/CategorizeAction.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/CategorizeAction.java new file mode 100644 index 0000000000000000000000000000000000000000..785f75f1c6b9291ef34fd68e7fd49e7de029a4c5 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/CategorizeAction.java @@ -0,0 +1,157 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.actions; + +import org.sleuthkit.autopsy.imageanalyzer.EurekaController; +import org.sleuthkit.autopsy.imageanalyzer.FileIDSelectionModel; +import org.sleuthkit.autopsy.imageanalyzer.FileUpdateEvent; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.Category; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute; +import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupKey; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import javafx.event.ActionEvent; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuItem; +import javafx.scene.input.KeyCodeCombination; +import javax.swing.JOptionPane; +import javax.swing.SwingWorker; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.ContentTag; +import org.sleuthkit.datamodel.TagName; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Adaptation of Tag Actions to enforce category-tag uniqueness + * + * TODO: since we are not using actionsGlobalContext anymore and this has + * diverged from autopsy action, make this extend from controlsfx Action + */ +public class CategorizeAction extends AddTagAction { + + private static final Logger LOGGER = Logger.getLogger(CategorizeAction.class.getName()); + + private final EurekaController controller; + + public CategorizeAction() { + super(); + this.controller = EurekaController.getDefault(); + } + + static public Menu getPopupMenu() { + return new CategoryMenu(); + } + + @Override + protected String getActionDisplayName() { + return "Categorize"; + } + + @Override + public void addTag(TagName tagName, String comment) { + Set<Long> selectedFiles = new HashSet<>(FileIDSelectionModel.getInstance().getSelected()); + + //TODO: should this get submitted to controller rather than a swingworker ? -jm + new SwingWorker<Object, Object>() { + + @Override + protected Object doInBackground() throws Exception { + Logger.getAnonymousLogger().log(Level.INFO, "categorizing{0} as {1}", new Object[]{selectedFiles.toString(), tagName.getDisplayName()}); + for (Long fileID : selectedFiles) { + + try { + DrawableFile file = controller.getFileFromId(fileID); + + Category oldCat = file.getCategory(); + // remove file from old category group + controller.getGroupManager().removeFromGroup(new GroupKey(DrawableAttribute.CATEGORY, oldCat), fileID); + + //remove old category tag if necessary + List<ContentTag> allContentTags = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByContent(file); + + for (ContentTag ct : allContentTags) { + //this is bad: treating tags as categories as long as thier names start with prefix + //TODO: abandon using tags for categories and instead add a new column to DrawableDB + if (ct.getName().getDisplayName().startsWith(Category.CATEGORY_PREFIX)) { + LOGGER.log(Level.INFO, "removing old category from {0}", file.getName()); + Case.getCurrentCase().getServices().getTagsManager().deleteContentTag(ct); + } + } + + if (tagName != Category.ZERO.getTagName()) { // no tags for cat-0 + Case.getCurrentCase().getServices().getTagsManager().addContentTag(file, tagName, comment); + } + //make sure rest of ui hears category change. + controller.handleFileUpdate( + new FileUpdateEvent(Collections.singleton(fileID), DrawableAttribute.CATEGORY)); + + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "Error categorizing result", ex); + JOptionPane.showMessageDialog(null, "Unable to categorize " + fileID + ".", "Categorizing Error", JOptionPane.ERROR_MESSAGE); + } + } + + refreshDirectoryTree(); + return null; + } + + @Override + protected void done() { + super.done(); + try { + get(); + } catch (InterruptedException | ExecutionException ex) { + LOGGER.log(Level.SEVERE, "unexpected exception while categorizing files", ex); + } + } + + }.execute(); + + } + + /** + * Instances of this class implement a context menu user interface for + * selecting a category + */ + static protected class CategoryMenu extends Menu { + + CategoryMenu() { + super("Categorize"); + + // Each category get an item in the sub-menu. Selecting one of these menu items adds + // a tag with the associated category. + for (final Category cat : Category.values()) { + + MenuItem categoryItem = new MenuItem(cat.getDisplayName()); + categoryItem.setOnAction((ActionEvent t) -> { + final CategorizeAction categorizeAction = new CategorizeAction(); + categorizeAction.addTag(cat.getTagName(), NO_COMMENT); + }); + categoryItem.setAccelerator(new KeyCodeCombination(cat.getHotKeycode())); + getItems().add(categoryItem); + } + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/Forward.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/Forward.java new file mode 100644 index 0000000000000000000000000000000000000000..fcd69ec5a4aa887dc531034351c21a55700731b6 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/Forward.java @@ -0,0 +1,50 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2014 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.actions; + +import javafx.event.ActionEvent; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import org.controlsfx.control.action.AbstractAction; +import org.sleuthkit.autopsy.imageanalyzer.EurekaController; + +/** + * + */ +public class Forward extends AbstractAction { + + private static final Image BACK_IMAGE = new Image("/org/sleuthkit/autopsy/imageanalyzer/images/arrow.png", 16, 16, true, true, true); + + private final EurekaController controller; + + public Forward(EurekaController controller) { + super("Forward"); + setGraphic(new ImageView(BACK_IMAGE)); + setAccelerator(new KeyCodeCombination(KeyCode.RIGHT, KeyCodeCombination.ALT_DOWN)); + this.controller = controller; + disabledProperty().bind(controller.getForwardStack().sizeProperty().isEqualTo(0)); + } + + @Override + public void handle(ActionEvent ae) { + controller.goForward(); + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/NextUnseenGroup.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/NextUnseenGroup.java new file mode 100644 index 0000000000000000000000000000000000000000..783456174195f8445ee30b6ee1f7696bb1873521 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/NextUnseenGroup.java @@ -0,0 +1,54 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-14 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.actions; + +import org.sleuthkit.autopsy.imageanalyzer.EurekaController; +import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupViewState; +import javafx.beans.Observable; +import javafx.event.ActionEvent; +import javafx.scene.image.ImageView; +import org.controlsfx.control.action.AbstractAction; + +/** + * + */ +public class NextUnseenGroup extends AbstractAction { + + private final EurekaController controller; + + public NextUnseenGroup(EurekaController controller) { + super("Next Unseen group"); + this.controller = controller; + setGraphic(new ImageView("/org/sleuthkit/autopsy/imageanalyzer/images/control-double.png")); + disabledProperty().set(controller.getGroupManager().getUnSeenGroups().size() <= 1); + controller.getGroupManager().getUnSeenGroups().addListener((Observable observable) -> { + disabledProperty().set(controller.getGroupManager().getUnSeenGroups().size() <= 1); + }); + } + + @Override + public void handle(ActionEvent event) { + if (controller.viewState() != null && controller.viewState().get() != null && controller.viewState().get().getGroup() != null) { + controller.getGroupManager().markGroupSeen(controller.viewState().get().getGroup()); + } + if (controller.getGroupManager().getUnSeenGroups().size() > 0) { + controller.pushGroup(GroupViewState.tile(controller.getGroupManager().getUnSeenGroups().get(0)), false); + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/OpenAction.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/OpenAction.java new file mode 100644 index 0000000000000000000000000000000000000000..80b58ae886e6f1f72733f93cd28ee2b1d6f3913f --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/OpenAction.java @@ -0,0 +1,121 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.actions; + +import java.awt.Component; +import javax.swing.JButton; +import javax.swing.JOptionPane; +import org.openide.awt.ActionID; +import org.openide.awt.ActionReference; +import org.openide.awt.ActionRegistration; +import org.openide.util.HelpCtx; +import org.openide.util.NbBundle.Messages; +import org.openide.util.actions.CallableSystemAction; +import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.core.Installer; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.imageanalyzer.EurekaController; +import org.sleuthkit.autopsy.imageanalyzer.EurekaModule; + +@ActionID(category = "Tools", + id = "org.sleuthkit.autopsy.imageanalyzer.OpenAction") +@ActionReference(path = "Menu/Tools" /* , position = 333 */) +@ActionRegistration( // iconBase = "org/sleuthkit/autopsy/imageanalyzer/images/lightbulb.png", + lazy = false, + displayName = "#CTL_OpenAction") +@Messages("CTL_OpenAction=Analyze Images/Videos") +public final class OpenAction extends CallableSystemAction { + + private final String EUREKA = "Analyze Images/Videos"; + + private static final boolean fxInited = Installer.isJavaFxInited(); + + private static final Logger LOGGER = Logger.getLogger(OpenAction.class.getName()); + + public OpenAction() { + super(); + } + + @Override + public boolean isEnabled() { + return Case.isCaseOpen() && fxInited && Case.getCurrentCase().getRootObjectsCount() > 0; + } + + /** Returns the toolbar component of this action + * + * @return component the toolbar button */ + @Override + public Component getToolbarPresenter() { + JButton toolbarButton = new JButton(this); + toolbarButton.setText(EUREKA); + toolbarButton.addActionListener(this); + + return toolbarButton; + } + + @Override + public void performAction() { + + //check case + if (!Case.existsCurrentCase()) { + return; + } + final Case currentCase = Case.getCurrentCase(); + + if (EurekaModule.isCaseStale(currentCase)) { + //case is stale, ask what to do + int answer = JOptionPane.showConfirmDialog(WindowManager.getDefault().getMainWindow(), "The image / video databse may be out of date. " + + "Do you want to update and listen for further ingest results?\n" + + " Choosing 'no' will display the out of date results." + + " Choosing 'cancel' will close the image /video analyzer", + "The image / video databse may be out of date. ", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE); + + switch (answer) { + case JOptionPane.YES_OPTION: + EurekaController.getDefault().setListeningEnabled(true); + //fall through + case JOptionPane.NO_OPTION: + EurekaModule.openTopComponent(); + break; + case JOptionPane.CANCEL_OPTION: + break; //do nothing + + } + } else { + //case is not stale, just open it + EurekaModule.openTopComponent(); + } + } + + @Override + public String getName() { + return EUREKA; + } + + @Override + public HelpCtx getHelpCtx() { + return HelpCtx.DEFAULT_HELP; + } + + @Override + public boolean asynchronous() { + return false; // run on edt + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/OpenHelpAction.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/OpenHelpAction.java new file mode 100644 index 0000000000000000000000000000000000000000..0fbf4062f82330658b60d14dd23f524ae230152a --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/OpenHelpAction.java @@ -0,0 +1,51 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.actions; + +import java.awt.Desktop; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.IOException; +import java.net.URI; +import org.openide.awt.ActionID; +import org.openide.awt.ActionReference; +import org.openide.awt.ActionRegistration; +import org.openide.util.Exceptions; +import org.openide.util.NbBundle.Messages; + +@ActionID( + category = "Help", + id = "org.sleuthkit.autopsy.imageanalyzer.actions.OpenHelpAction" +) +@ActionRegistration( + displayName = "#CTL_OpenHelpAction" +) +@ActionReference(path = "Menu/Help", position = 350) +@Messages("CTL_OpenHelpAction=Image / Video Analyzer Help") +public final class OpenHelpAction implements ActionListener { + + @Override + public void actionPerformed(ActionEvent e) { + try { + Desktop.getDesktop().browse(URI.create("http://www.google.com")); + } catch (IOException ex) { + Exceptions.printStackTrace(ex); + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/SwingMenuItemAdapter.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/SwingMenuItemAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..2d35d2f92641b1f91814147a8fe7ead166a138f6 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/actions/SwingMenuItemAdapter.java @@ -0,0 +1,87 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-14 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.actions; + +import javafx.event.ActionEvent; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuItem; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; +import javax.swing.MenuElement; + +public class SwingMenuItemAdapter extends MenuItem { + + JMenuItem jMenuItem; + + SwingMenuItemAdapter(final JMenuItem jMenuItem) { + super(jMenuItem.getText()); + this.jMenuItem = jMenuItem; + setOnAction((ActionEvent t) -> { + jMenuItem.doClick(); + }); + } + + public static MenuItem create(MenuElement jmenuItem) { + if (jmenuItem instanceof JMenu) { + return new SwingMenuAdapter((JMenu) jmenuItem); + } else if (jmenuItem instanceof JPopupMenu) { + return new SwingMenuAdapter((JPopupMenu) jmenuItem); + } else { + return new SwingMenuItemAdapter((JMenuItem) jmenuItem); + } + + } +} + +class SwingMenuAdapter extends Menu { + + private final MenuElement jMenu; + + SwingMenuAdapter(final JMenu jMenu) { + super(jMenu.getText()); + this.jMenu = jMenu; + buildChildren(jMenu); + + } + + SwingMenuAdapter(JPopupMenu jPopupMenu) { + super(jPopupMenu.getLabel()); + this.jMenu = jPopupMenu; + + buildChildren(jMenu); + } + + private void buildChildren(MenuElement jMenu) { + + for (MenuElement menuE : jMenu.getSubElements()) { + if (menuE instanceof JMenu) { + getItems().add(SwingMenuItemAdapter.create((JMenu) menuE)); + } else if (menuE instanceof JMenuItem) { + getItems().add(SwingMenuItemAdapter.create((JMenuItem) menuE)); + } else if (menuE instanceof JPopupMenu) { + buildChildren(menuE); + } else { + + System.out.println(menuE.toString()); +// throw new UnsupportedOperationException(); + } + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/Category.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/Category.java new file mode 100644 index 0000000000000000000000000000000000000000..3d9e9dfc1c5611625eb7497cdfc9cfe6990394a5 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/Category.java @@ -0,0 +1,160 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.datamodel; + +import org.sleuthkit.autopsy.imageanalyzer.TagUtils; +import org.sleuthkit.autopsy.imageanalyzer.actions.CategorizeAction; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.scene.control.MenuItem; +import javafx.scene.control.SplitMenuButton; +import javafx.scene.image.ImageView; +import javafx.scene.input.KeyCode; +import javafx.scene.paint.Color; +import javax.annotation.concurrent.GuardedBy; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.TagName; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * + */ +public enum Category implements Comparable<Category> { + + ZERO(Color.LIGHTGREY, 0, "CAT-0, Uncategorized"), + ONE(Color.RED, 1, "CAT-1, Child Exploitation (Illegal)"), + TWO(Color.ORANGE, 2, "CAT-2, Child Exploitation (Non-Illegal/Age Difficult)"), + THREE(Color.YELLOW, 3, "CAT-3, CGI/Animation (Child Exploitive)"), + FOUR(Color.BISQUE, 4, "CAT-4, Exemplar/Comparison (Internal Use Only)"), + FIVE(Color.GREEN, 5, "CAT-5, Non-pertinent"); + + final static private Map<String, Category> nameMap = new HashMap(); + + private static List<Category> valuesList = Arrays.asList(values()); + + static { + for (Category cat : values()) { + nameMap.put(cat.displayName, cat); + } + } + + @GuardedBy("listeners") + private final static Set<CategoryListener> listeners = new HashSet<>(); + + public static void fireChange(Collection<Long> ids) { + synchronized (listeners) { + for (CategoryListener list : listeners) { + list.handleCategoryChanged(ids); + } + } + } + + public static void registerListener(CategoryListener aThis) { + synchronized (listeners) { + listeners.add(aThis); + } + } + + public static void unregisterListener(CategoryListener aThis) { + synchronized (listeners) { + listeners.remove(aThis); + } + } + + public KeyCode getHotKeycode() { + return KeyCode.getKeyCode(Integer.toString(id)); + } + + public static final String CATEGORY_PREFIX = "CAT-"; + + private TagName tagName; + + public static List<Category> valuesList() { + return valuesList; + } + + private Color color; + + private String displayName; + + private int id; + + public Color getColor() { + return color; + } + + public String getDisplayName() { + return displayName; + } + + @Override + public String toString() { + return displayName; + } + + static public Category fromDisplayName(String displayName) { + return nameMap.get(displayName); + } + + private Category(Color color, int id, String name) { + this.color = color; + this.displayName = name; + this.id = id; + } + + public TagName getTagName() { + + if (tagName == null) { + try { + tagName = TagUtils.getTagName(displayName); + } catch (TskCoreException ex) { + Logger.getLogger(Category.class.getName()).log(Level.SEVERE, "failed to get TagName for " + displayName, ex); + } + } + return tagName; + } + + public MenuItem createSelCatMenuItem(final SplitMenuButton catSelectedMenuButton) { + final MenuItem menuItem = new MenuItem(this.getDisplayName(), new ImageView(DrawableAttribute.CATEGORY.getIcon())); + menuItem.setOnAction(new EventHandler<ActionEvent>() { + @Override + public void handle(ActionEvent t) { + // EurekaTileSelectionModel.getInstance().clearAndSelectAll(getTiles().toArray(new DrawableTile[getTiles().size()])); + new CategorizeAction().addTag(Category.this.getTagName(), ""); + catSelectedMenuButton.setText(Category.this.getDisplayName()); + catSelectedMenuButton.setOnAction(this); + } + }); + return menuItem; + } + + public static interface CategoryListener { + + public void handleCategoryChanged(Collection<Long> ids); + + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/DrawableAttribute.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/DrawableAttribute.java new file mode 100644 index 0000000000000000000000000000000000000000..32a8a120fc72fe981424b8ac911ece81c259b02a --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/DrawableAttribute.java @@ -0,0 +1,141 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.datamodel; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import javafx.beans.property.ReadOnlyStringWrapper; +import javafx.beans.property.StringProperty; +import javafx.scene.image.Image; +import org.apache.commons.lang3.StringUtils; +import org.sleuthkit.datamodel.TagName; + +/** psuedo-enum of attributes to filter, sort, and group on. They + * mostly correspond to the columns in the db. + * + * TODO: Review and refactor DrawableAttribute related code with an eye to usage + * of type paramaters and multivalued attributes + */ +public class DrawableAttribute<T> { + + public final static DrawableAttribute<String> NAME + = new DrawableAttribute<>(AttributeName.NAME, "Name", true, "folder-rename.png"); + + public final static DrawableAttribute<Boolean> ANALYZED + = new DrawableAttribute<>(AttributeName.ANALYZED, "Analyzed", true, ""); + + /** since categories are really just tags in autopsy, they are not dealt + * with in the DrawableDB. they have special code in various places + * to make this transparent. + * + * //TODO: this had lead to awkward hard to maintain code, and little + * advantage. move categories into DrawableDB + */ + public final static DrawableAttribute<Category> CATEGORY + = new DrawableAttribute<>(AttributeName.CATEGORY, "Category", false, "category-icon.png"); + + public final static DrawableAttribute<Collection<TagName>> TAGS + = new DrawableAttribute<>(AttributeName.TAGS, "Tags", false, "tag_red.png"); + + public final static DrawableAttribute<String> PATH + = new DrawableAttribute<>(AttributeName.PATH, "Path", true, "folder_picture.png"); + + public final static DrawableAttribute<Long> CREATED_TIME + = new DrawableAttribute<>(AttributeName.CREATED_TIME, "Created Time", true, "clock--plus.png"); + + public final static DrawableAttribute<Long> MODIFIED_TIME + = new DrawableAttribute<>(AttributeName.MODIFIED_TIME, "Modified Time", true, "clock--pencil.png"); + + public final static DrawableAttribute<String> MAKE + = new DrawableAttribute<>(AttributeName.MAKE, "Camera Make", true, "camera.png"); + + public final static DrawableAttribute<String> MODEL + = new DrawableAttribute<>(AttributeName.MODEL, "Camera Model", true, "camera.png"); + + //TODO: should this be DrawableAttribute<Collection<String>>? + public final static DrawableAttribute<String> HASHSET + = new DrawableAttribute<>(AttributeName.HASHSET, "Hashset", false, "hashset_hits.png"); + + public final static DrawableAttribute<Long> OBJ_ID + = new DrawableAttribute<>(AttributeName.OBJ_ID, "Internal Object ID", true, ""); + + public final static DrawableAttribute<Number> WIDTH + = new DrawableAttribute<>(AttributeName.WIDTH, "Width", true, "arrow-resize.png"); + + public final static DrawableAttribute<Number> HEIGHT + = new DrawableAttribute<>(AttributeName.HEIGHT, "Height", true, "arrow-resize-090.png"); + + final private static List< DrawableAttribute<?>> groupables + = Arrays.asList(PATH, HASHSET, CATEGORY, TAGS, MAKE, MODEL); + + final private static List<DrawableAttribute<?>> values + = Arrays.asList(NAME, ANALYZED, CATEGORY, TAGS, PATH, CREATED_TIME, + MODIFIED_TIME, HASHSET, CATEGORY, MAKE, MODEL, OBJ_ID, + WIDTH, HEIGHT); + + private DrawableAttribute(AttributeName name, String displayName, Boolean isDBColumn, String imageName) { + this.attrName = name; + this.displayName = new ReadOnlyStringWrapper(displayName); + this.isDBColumn = isDBColumn; + this.imageName = imageName; + } + + private Image icon; + + public final boolean isDBColumn; + + public final AttributeName attrName; + + private final StringProperty displayName; + + private final String imageName; + + public Image getIcon() { + if (icon == null) { + if (StringUtils.isBlank(imageName) == false) { + this.icon = new Image("org/sleuthkit/autopsy/imageanalyzer/images/" + imageName, true); + } + } + return icon; + } + + public static List<DrawableAttribute<?>> getGroupableAttrs() { + return groupables; + } + + public static List<DrawableAttribute<?>> getValues() { + return Collections.unmodifiableList(values); + } + + public StringProperty displayName() { + return displayName; + } + + public String getDisplayName() { + return displayName.get(); + } + + public static enum AttributeName { + + NAME, ANALYZED, CATEGORY, TAGS, PATH, CREATED_TIME, MODIFIED_TIME, MAKE, + MODEL, HASHSET, OBJ_ID, WIDTH, HEIGHT; + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/DrawableDB.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/DrawableDB.java new file mode 100644 index 0000000000000000000000000000000000000000..766b11047536d97315bcaec5cbf1a5945fc099cb --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/DrawableDB.java @@ -0,0 +1,1146 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-14 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.datamodel; + +import java.io.File; +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.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.logging.Level; +import javax.swing.SortOrder; +import org.apache.commons.lang.StringUtils; +import org.openide.util.Exceptions; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.imageanalyzer.EurekaController; +import org.sleuthkit.autopsy.imageanalyzer.FileUpdateEvent; +import org.sleuthkit.autopsy.imageanalyzer.FileUpdateListener; +import static org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute.AttributeName.CATEGORY; +import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupKey; +import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupManager; +import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupSortBy; +import static org.sleuthkit.autopsy.imageanalyzer.grouping.GroupSortBy.GROUP_BY_VALUE; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.ContentTag; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TagName; +import org.sleuthkit.datamodel.TskCoreException; +import org.sqlite.SQLiteJDBCLoader; + +/** This class is the public interface to the Image / Video Analyzer SQLite + * database. This class borrows a lot of ideas and techniques (for good or ill) + * from {@link SleuthkitCase}. + * + * TODO: Creating an abstract base class for sqlite databases* may make sense + * in the future. see also {@link EventsDB} */ +public class DrawableDB { + + private static final java.util.logging.Logger LOGGER = Logger.getLogger(DrawableDB.class.getName()); + + //column name constants////////////////////// + private static final String ANALYZED = "analyzed"; + + private static final String OBJ_ID = "obj_id"; + + private static final String HASH_SET_NAME = "hash_set_name"; + + private final PreparedStatement insertHashSetStmt; + + private final PreparedStatement groupSeenQueryStmt; + + private final PreparedStatement insertGroupStmt; + + private final List<PreparedStatement> preparedStatements = new ArrayList<>(); + + private final PreparedStatement removeFileStmt; + + private final PreparedStatement updateGroupStmt; + + private final PreparedStatement selectHashSetStmt; + + private final PreparedStatement selectHashSetNamesStmt; + + private final PreparedStatement insertHashHitStmt; + + private final PreparedStatement insertFileStmt; + + private final PreparedStatement pathGroupStmt; + + private final PreparedStatement nameGroupStmt; + + private final PreparedStatement created_timeGroupStmt; + + private final PreparedStatement modified_timeGroupStmt; + + private final PreparedStatement makeGroupStmt; + + private final PreparedStatement modelGroupStmt; + + private final PreparedStatement analyzedGroupStmt; + + private final PreparedStatement hashSetGroupStmt; + + /** map from {@link DrawableAttribute} to the {@link PreparedStatement} thet + * is used to select groups for that attribute */ + private final Map<DrawableAttribute, PreparedStatement> groupStatementMap = new HashMap<>(); + + /** list of observers to be notified if the database changes */ + private final HashSet<FileUpdateListener> updateListeners = new HashSet<>(); + + private GroupManager manager; + + private EurekaController controller; + + private final String dbPath; + + volatile private Connection con; + + private static final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(true); //use fairness policy + + private static final Lock DBLock = rwLock.writeLock(); //using exclusing lock for all db ops for now + + static {//make sure sqlite driver is loaded // possibly redundant + try { + Class.forName("org.sqlite.JDBC"); + } catch (ClassNotFoundException ex) { + LOGGER.log(Level.SEVERE, "Failed to load sqlite JDBC driver", ex); + } + } + + //////////////general database logic , mostly borrowed from sleuthkitcase + /** + * Lock to protect against concurrent write accesses to case database and to + * block readers while database is in write transaction. Should be utilized + * by all db code where underlying storage supports max. 1 concurrent writer + * MUST always call dbWriteUnLock() as early as possible, in the same thread + * where dbWriteLock() was called + */ + public static void dbWriteLock() { + //Logger.getLogger("LOCK").log(Level.INFO, "Locking " + rwLock.toString()); + DBLock.lock(); + } + + /** + * Release previously acquired write lock acquired in this thread using + * dbWriteLock(). Call in "finally" block to ensure the lock is always + * released. + */ + public static void dbWriteUnlock() { + //Logger.getLogger("LOCK").log(Level.INFO, "UNLocking " + rwLock.toString()); + DBLock.unlock(); + } + + /** + * Lock to protect against read while it is in a write transaction + * state. + * Supports multiple concurrent readers if there is no writer. MUST + * always call dbReadUnLock() as early as possible, in the same thread + * where dbReadLock() was called. + */ + void dbReadLock() { + DBLock.lock(); + } + + /** + * Release previously acquired read lock acquired in this thread using + * dbReadLock(). Call in "finally" block to ensure the lock is always + * released. + */ + void dbReadUnlock() { + DBLock.unlock(); + } + + /** @param dbPath the path to the db file + * + * @throws SQLException if there is problem creating or configuring the db */ + private DrawableDB(String dbPath) throws SQLException, ExceptionInInitializerError { + this.dbPath = dbPath; + + if (initializeDB()) { + insertFileStmt = prepareStatement( + "INSERT OR REPLACE INTO drawable_files (obj_id , path, name, created_time, modified_time, make, model, analyzed) " + + "VALUES (?,?,?,?,?,?,?,?)"); + 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 natural join hash_sets natural join hash_set_hits where hash_sets.hash_set_name = ?", DrawableAttribute.HASHSET); + + updateGroupStmt = prepareStatement("update groups set seen = 1 where value = ? and attribute = ?"); + insertGroupStmt = prepareStatement("insert or replace into groups (value, attribute) values (?,?)"); + + 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 = ?"); + + insertHashHitStmt = prepareStatement("insert or ignore into hash_set_hits (hash_set_id, obj_id) values (?,?)"); + + } else { + throw new ExceptionInInitializerError(); + } + } + + /** create PreparedStatement with the supplied string, and add the new + * statement to the list of PreparedStatements used in {@link DrawableDB#closeStatements() + * + * @param stmtString the string representation of the sqlite statement to + * prepare + * + * @return the prepared statement + * + * @throws SQLException if unable to prepare the statement */ + private PreparedStatement prepareStatement(String stmtString) throws SQLException { + PreparedStatement prepareStatement = con.prepareStatement(stmtString); + preparedStatements.add(prepareStatement); + return prepareStatement; + } + + /** calls {@link DrawableDB#prepareStatement(java.lang.String) , + * and then add the statement to the groupStatmentMap used to lookup + * statements by the attribute/column they group on + * + * @param stmtString the string representation of the sqlite statement to + * prepare + * @param attr the {@link DrawableAttribute} this query groups by + * + * @return the prepared statement + * + * @throws SQLExceptionif unable to prepare the statement */ + private PreparedStatement prepareStatement(String stmtString, DrawableAttribute<?> attr) throws SQLException { + PreparedStatement prepareStatement = prepareStatement(stmtString); + if (attr != null) { + groupStatementMap.put(attr, prepareStatement); + } + + return prepareStatement; + } + + /** public factory method. Creates and opens a connection to a new + * database * at the given path. + * + * @param dbPath + * + * @return + */ + public static DrawableDB getDrawableDB(String dbPath, EurekaController controller) { + + try { + DrawableDB drawableDB = new DrawableDB(dbPath + File.separator + "drawable.db"); + drawableDB.controller = controller; + return drawableDB; + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "sql error creating database connection", ex); + return null; + } catch (ExceptionInInitializerError ex) { + LOGGER.log(Level.SEVERE, "error creating database connection", ex); + return null; + } + } + + private void setPragmas() throws SQLException { + + //this should match Sleuthkit db setupt + try (Statement statement = con.createStatement()) { + //reduce i/o operations, we have no OS crash recovery anyway + statement.execute("PRAGMA synchronous = OFF;"); + //allow to query while in transaction - no need read locks + statement.execute("PRAGMA read_uncommitted = True;"); + + //TODO: do we need this? + statement.execute("PRAGMA foreign_keys = ON"); + + //TODO: test this + statement.execute("PRAGMA journal_mode = MEMORY"); +// + //we don't use this feature, so turn it off for minimal speed up on queries + //this is deprecated and not recomended + statement.execute("PRAGMA count_changes = OFF;"); + //this made a big difference to query speed + statement.execute("PRAGMA temp_store = MEMORY"); + //this made a modest improvement in query speeds + statement.execute("PRAGMA cache_size = 50000"); + //we never delete anything so... + statement.execute("PRAGMA auto_vacuum = 0"); + } + + try { + LOGGER.log(Level.INFO, String.format("sqlite-jdbc version %s loaded in %s mode", + SQLiteJDBCLoader.getVersion(), SQLiteJDBCLoader.isNativeMode() + ? "native" : "pure-java")); + } catch (Exception exception) { + LOGGER.log(Level.WARNING, "exception while checking sqlite-jdbc version and mode", exception); + } + + } + + /** create the table and indices if they don't already exist + * + * @return the number of rows in the table , count > 0 indicating an + * existing table */ + private boolean initializeDB() { + try { + if (isClosed()) { + openDBCon(); + } + setPragmas(); + + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "problem accessing database", ex); + return false; + } + try (Statement stmt = con.createStatement()) { + String sql = "CREATE TABLE if not exists drawable_files " + + "( obj_id INTEGER PRIMARY KEY, " + + " path VARCHAR(255), " + + " name VARCHAR(255), " + + " created_time integer, " + + " modified_time integer, " + + " make VARCHAR(255), " + + " model VARCHAR(255), " + + " analyzed integer DEFAULT 0)"; + stmt.execute(sql); + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "problem creating drawable_files table", ex); + return false; + } + + try (Statement stmt = con.createStatement()) { + String sql = "CREATE TABLE if not exists groups " + + "(group_id INTEGER PRIMARY KEY, " + + " value VARCHAR(255) not null, " + + " attribute VARCHAR(255) not null, " + + " seen integer DEFAULT 0, " + + " UNIQUE(value, attribute) )"; + stmt.execute(sql); + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "problem creating groups table", ex); + return false; + } + + try (Statement stmt = con.createStatement()) { + String sql = "CREATE TABLE if not exists hash_sets " + + "( hash_set_id INTEGER primary key," + + " hash_set_name VARCHAR(255) UNIQUE NOT NULL)"; + stmt.execute(sql); + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "problem creating hash_sets table", ex); + return false; + } + + try (Statement stmt = con.createStatement()) { + String sql = "CREATE TABLE if not exists hash_set_hits " + + "(hash_set_id INTEGER REFERENCES hash_sets(hash_set_id) not null, " + + " obj_id INTEGER REFERENCES drawable_files(obj_id) not null, " + + " PRIMARY KEY (hash_set_id, obj_id))"; + stmt.execute(sql); + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "problem creating hash_set_hits table", ex); + return false; + } + + try (Statement stmt = con.createStatement()) { + String sql = "CREATE UNIQUE INDEX if not exists obj_id_idx ON drawable_files(obj_id)"; + stmt.execute(sql); + } catch (SQLException ex) { + LOGGER.log(Level.WARNING, "problem creating obj_id_idx", ex); + } + + try (Statement stmt = con.createStatement()) { + String sql = "CREATE INDEX if not exists path_idx ON drawable_files(path)"; + stmt.execute(sql); + } catch (SQLException ex) { + LOGGER.log(Level.WARNING, "problem creating path_idx", ex); + } + + try (Statement stmt = con.createStatement()) { + String sql = "CREATE INDEX if not exists name_idx ON drawable_files(name)"; + stmt.execute(sql); + } catch (SQLException ex) { + LOGGER.log(Level.WARNING, "problem creating name_idx", ex); + } + + try (Statement stmt = con.createStatement()) { + String sql = "CREATE INDEX if not exists make_idx ON drawable_files(make)"; + stmt.execute(sql); + } catch (SQLException ex) { + LOGGER.log(Level.WARNING, "problem creating make_idx", ex); + } + + try (Statement stmt = con.createStatement()) { + String sql = "CREATE INDEX if not exists model_idx ON drawable_files(model)"; + stmt.execute(sql); + } catch (SQLException ex) { + LOGGER.log(Level.WARNING, "problem creating model_idx", ex); + } + + try (Statement stmt = con.createStatement()) { + String sql = "CREATE INDEX if not exists analyzed_idx ON drawable_files(analyzed)"; + stmt.execute(sql); + } catch (SQLException ex) { + LOGGER.log(Level.WARNING, "problem creating analyzed_idx", ex); + } + + return true; + } + + @Override + public void finalize() throws Throwable { + try { + closeDBCon(); + } finally { + super.finalize(); + } + } + + public void closeDBCon() { + if (con != null) { + try { + closeStatements(); + con.close(); + } catch (SQLException ex) { + LOGGER.log(Level.WARNING, "Failed to close connection to drawable.db", ex); + } + } + con = null; + } + + public void openDBCon() { + try { + if (con == null || con.isClosed()) { + con = DriverManager.getConnection("jdbc:sqlite:" + dbPath); + } + } catch (SQLException ex) { + LOGGER.log(Level.WARNING, "Failed to open connection to drawable.db", ex); + } + } + + public boolean isClosed() throws SQLException { + if (con == null) { + return true; + } + return con.isClosed(); + } + + /** get all the hash set names used in the db + * + * @return a set of the names of all the hash sets that have hash set hits */ + public Set<String> getHashSetNames() { + Set<String> names = new HashSet<>(); + // "SELECT DISTINCT hash_set_name FROM hash_sets" + dbReadLock(); + try (ResultSet rs = selectHashSetNamesStmt.executeQuery();) { + while (rs.next()) { + names.add(rs.getString(HASH_SET_NAME)); + } + } catch (SQLException sQLException) { + LOGGER.log(Level.WARNING, "failed to get hash set names", sQLException); + } finally { + dbReadUnlock(); + } + return names; + } + + public boolean isGroupSeen(GroupKey groupKey) { + dbReadLock(); + try { + groupSeenQueryStmt.clearParameters(); + groupSeenQueryStmt.setString(1, groupKey.getValueDisplayName()); + groupSeenQueryStmt.setString(2, groupKey.getAttribute().attrName.toString()); + try (ResultSet rs = groupSeenQueryStmt.executeQuery()) { + while (rs.next()) { + return rs.getBoolean("seen"); + } + } + } catch (SQLException ex) { + Exceptions.printStackTrace(ex); + } finally { + dbReadUnlock(); + } + return false; + } + + public void markGroupSeen(GroupKey gk) { + dbWriteLock(); + try { + //PreparedStatement updateGroup = con.prepareStatement("update groups set seen = 1 where value = ? and attribute = ?"); + updateGroupStmt.clearParameters(); + updateGroupStmt.setString(1, gk.getValueDisplayName()); + updateGroupStmt.setString(2, gk.getAttribute().attrName.toString()); + updateGroupStmt.execute(); + } catch (SQLException ex) { + Exceptions.printStackTrace(ex); + } + dbWriteUnlock(); + } + + public boolean removeFile(long id) { + DrawableTransaction trans = beginTransaction(); + boolean removeFile = removeFile(id, trans); + commitTransaction(trans, true); + return removeFile; + } + + public void updateFile(DrawableFile f) { + DrawableTransaction trans = beginTransaction(); + updatefile(f, trans); + commitTransaction(trans, true); + } + + /** + * use transactions to update files + * + * @param f + * @param tr + * + * + * + */ + public void updatefile(DrawableFile f, DrawableTransaction tr) { + + //TODO: implement batch version -jm + if (tr.isClosed()) { + throw new IllegalArgumentException("can't update database with closed transaction"); + } + dbWriteLock(); + try { + + // "INSERT OR REPLACE INTO drawable_files (path, name, created_time, modified_time, make, model, analyzed)" + insertFileStmt.setLong(1, f.getId()); + insertFileStmt.setString(2, f.getDrawablePath()); + insertFileStmt.setString(3, f.getName()); + insertFileStmt.setLong(4, f.getCrtime()); + insertFileStmt.setLong(5, f.getMtime()); + insertFileStmt.setString(6, f.getMake()); + insertFileStmt.setString(7, f.getModel()); + insertFileStmt.setInt(8, f.isAnalyzed() ? 1 : 0); + insertFileStmt.executeUpdate(); + + final List<String> hashSetNames = (List<String>) f.getValueOfAttribute(DrawableAttribute.HASHSET); + + if (hashSetNames.isEmpty() == false) { + for (String name : hashSetNames) { + + // "insert or ignore into hash_sets (hash_set_name) values (?)" + insertHashSetStmt.setString(1, name); + insertHashSetStmt.executeUpdate(); + + //TODO: use nested select to get hash_set_id rather than seperate statement/query + //"select hash_set_id from hash_sets where hash_set_name = ?" + selectHashSetStmt.setString(1, name); + try (ResultSet rs = selectHashSetStmt.executeQuery()) { + while (rs.next()) { + int hashsetID = rs.getInt("hash_set_id"); + //"insert or ignore into hash_set_hits (hash_set_id, obj_id) values (?,?)"; + insertHashHitStmt.setInt(1, hashsetID); + insertHashHitStmt.setLong(2, f.getId()); + insertHashHitStmt.executeUpdate(); + break; + } + } + } + } + + //and update all groups this file is in + for (DrawableAttribute attr : DrawableAttribute.getGroupableAttrs()) { + Object valueOfAttribute = f.getValueOfAttribute(attr); + Collection vals; + if (valueOfAttribute instanceof Collection) { + vals = (Collection) valueOfAttribute; + } else { + vals = Collections.singleton(valueOfAttribute); + } + for (Object val : vals) { + insertGroup(val.toString(), attr); + } + } + + tr.addUpdatedFile(f.getId()); + + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "failed to update file" + f.getName(), ex); + } finally { + dbWriteUnlock(); + } + } + + public DrawableTransaction beginTransaction() { + return new DrawableTransaction(); + } + + public void commitTransaction(DrawableTransaction tr, Boolean notify) { + if (tr.isClosed()) { + throw new IllegalArgumentException("can't close already closed transaction"); + } + tr.commit(notify); + } + + public void addUpdatedFileListener(FileUpdateListener l) { + updateListeners.add(l); + } + + /** + * + * @param f the value of f + */ + private void fireUpdatedFiles(Collection<Long> f) { + for (FileUpdateListener listener : updateListeners) { + listener.handleFileUpdate(new FileUpdateEvent(f, null, FileUpdateEvent.UpdateType.FILE_UPDATED)); + } + } + + private void fireRemovedFiles(Collection<Long> f) { + for (FileUpdateListener listener : updateListeners) { + listener.handleFileUpdate(new FileUpdateEvent(f, null, FileUpdateEvent.UpdateType.FILE_REMOVED)); + } + } + + public Boolean isFileAnalyzed(DrawableFile f) { + return isFileAnalyzed(f.getId()); + } + + public Boolean isFileAnalyzed(long fileId) { + dbReadLock(); + try (Statement stmt = con.createStatement(); + ResultSet analyzedQuery = stmt.executeQuery("select analyzed from drawable_files where obj_id = " + fileId)) { + while (analyzedQuery.next()) { + return analyzedQuery.getBoolean(ANALYZED); + } + } catch (SQLException ex) { + Exceptions.printStackTrace(ex); + } finally { + dbReadUnlock(); + } + + return false; + } + + public Boolean areFilesAnalyzed(Collection<Long> fileIds) { + + 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, ", ") + ")")) { + while (analyzedQuery.next()) { + return analyzedQuery.getInt(ANALYZED) == fileIds.size(); + } + } catch (SQLException ex) { + LOGGER.log(Level.WARNING, "problem counting analyzed files: ", ex); + } finally { + dbReadUnlock(); + } + + return false; + } + + public Boolean isGroupAnalyzed(GroupKey gk) { + dbReadLock(); + try { + List<Long> fileIDsInGroup = getFileIDsInGroup(gk); + + 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(fileIDsInGroup, ", ") + ")")) { + while (analyzedQuery.next()) { + return analyzedQuery.getInt(ANALYZED) == fileIDsInGroup.size(); + } + } catch (SQLException ex) { + LOGGER.log(Level.WARNING, "problem counting analyzed files: ", ex); + } + } catch (TskCoreException tskCoreException) { + LOGGER.log(Level.WARNING, "problem counting analyzed files: ", tskCoreException); + } finally { + dbReadUnlock(); + } + + return false; + } + + /** + * Find and return list of all ids of files matching the specific + * Where clause + * + * @param sqlWhereClause a SQL where clause appropriate for the desired + * files (do not begin the WHERE clause with the word WHERE!) + * + * @return a list of file ids each of which satisfy the given WHERE clause + * + * @throws TskCoreException + */ + public List<Long> findAllFileIdsWhere(String sqlWhereClause) throws TskCoreException { + Statement statement = null; + ResultSet rs = null; + List<Long> ret = new ArrayList<>(); + dbReadLock(); + try { + statement = con.createStatement(); + rs = statement.executeQuery("SELECT obj_id FROM drawable_files WHERE " + sqlWhereClause); + while (rs.next()) { + ret.add(rs.getLong(1)); + } + } catch (SQLException e) { + throw new TskCoreException("SQLException thrown when calling 'DrawableDB.findAllFileIdsWhere(): " + sqlWhereClause, e); + } finally { + if (rs != null) { + try { + rs.close(); + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "Error closing result set after executing findAllFileIdsWhere", ex); + } + } + if (statement != null) { + try { + statement.close(); + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "Error closing statement after executing findAllFileIdsWhere", ex); + } + } + dbReadUnlock(); + } + return ret; + } + + /** + * + * + * + * @param groupBy + * @param sortBy + * @param sortOrder + * + * @return + */ + public <A> List<A> findValuesForAttribute(DrawableAttribute<A> groupBy, GroupSortBy sortBy, SortOrder sortOrder) { + + List<A> vals = new ArrayList<>(); + + switch (groupBy.attrName) { + case ANALYZED: + case CATEGORY: + case HASHSET: + //these are somewhat special cases for now as they have fixed values, or live in the main autopsy database + //they should have special handling at a higher level of the stack. + throw new UnsupportedOperationException(); + default: + dbReadLock(); + //TODO: convert this to prepared statement + StringBuilder query = new StringBuilder("select " + groupBy.attrName.name() + ", count(*) from drawable_files group by " + groupBy.attrName.name()); + + String orderByClause = ""; + switch (sortBy) { + case GROUP_BY_VALUE: + orderByClause = " order by " + groupBy.attrName.name(); + break; + case FILE_COUNT: + orderByClause = " order by count(*)"; + break; + case NONE: +// case PRIORITY: + break; + } + + query.append(orderByClause); + + if (orderByClause.equals("") == false) { + String sortOrderClause = ""; + + switch (sortOrder) { + case DESCENDING: + sortOrderClause = " DESC"; + break; + case ASCENDING: + sortOrderClause = " ASC"; + break; + default: + orderByClause = ""; + } + query.append(sortOrderClause); + } + + try (Statement stmt = con.createStatement(); + ResultSet valsResults = stmt.executeQuery(query.toString())) { + + while (valsResults.next()) { + final Object object = valsResults.getObject(groupBy.attrName.name()); + vals.add((A) object); + } + } catch (SQLException ex) { + LOGGER.log(Level.WARNING, "Unable to get values for attribute", ex); + } finally { + dbReadUnlock(); + } + } + + return vals; + } + + public void insertGroup(final String value, DrawableAttribute groupBy) { + dbWriteLock(); + + try { + //PreparedStatement insertGroup = con.prepareStatement("insert or ignore into groups (value, attribute, seen) values (?,?,0)"); + insertGroupStmt.clearParameters(); + insertGroupStmt.setString(1, value); + insertGroupStmt.setString(2, groupBy.attrName.name()); + insertGroupStmt.execute(); + } catch (SQLException sQLException) { + LOGGER.log(Level.SEVERE, "Unable to insert group", sQLException); + } finally { + dbWriteUnlock(); + } + } + + /** + * @param id the obj_id of the file to return + * @param analyzed the analyzed state of the file + * + * @return a DrawableFile for the given obj_id and analyzed state + * + * @throws TskCoreException if unable to get a file from the currently open + * {@link SleuthkitCase} + */ + private DrawableFile getFileFromID(Long id, boolean analyzed) throws TskCoreException { + try { + return DrawableFile.create(controller.getSleuthKitCase().getAbstractFileById(id), analyzed); + } catch (IllegalStateException ex) { + LOGGER.log(Level.SEVERE, "there is no case open; failed to load file with id: " + id, ex); + return null; + } + } + + /** + * @param id the obj_id of the file to return + * + * @return a DrawableFile for the given obj_id + * + * @throws TskCoreException if unable to get a file from the currently open + * {@link SleuthkitCase} + */ + public DrawableFile getFileFromID(Long id) throws TskCoreException { + try { + return DrawableFile.create(controller.getSleuthKitCase().getAbstractFileById(id), + areFilesAnalyzed(Collections.singleton(id))); + } catch (IllegalStateException ex) { + LOGGER.log(Level.SEVERE, "there is no case open; failed to load file with id: " + id, ex); + return null; + } + } + + public List<Long> getFileIDsInGroup(GroupKey groupKey) throws TskCoreException { + + if (groupKey.getAttribute().isDBColumn) { + switch (groupKey.getAttribute().attrName) { + case CATEGORY: + return manager.getFileIDsWithCategory((Category) groupKey.getValue()); + case TAGS: + return manager.getFileIDsWithTag((TagName) groupKey.getValue()); + } + } + List<Long> files = new ArrayList<>(); + dbReadLock(); + try { + PreparedStatement statement = getGroupStatment(groupKey.getAttribute()); + statement.setObject(1, groupKey.getValue()); + + try (ResultSet valsResults = statement.executeQuery()) { + while (valsResults.next()) { + files.add(valsResults.getLong(OBJ_ID)); + } + } + } catch (SQLException ex) { + LOGGER.log(Level.WARNING, "failed to get file for group:" + groupKey.getAttribute() + " == " + groupKey.getValue(), ex); + } finally { + dbReadUnlock(); + } + + return files; + } + + public List<DrawableFile> getFilesInGroup(GroupKey key) throws TskCoreException { + List<DrawableFile> files = new ArrayList<>(); + dbReadLock(); + try { + PreparedStatement statement = null; + + /* I hate this! not flexible/generic/maintainable we could have the + * DrawableAttribute provide/create/configure the correct statement + * but they shouldn't be coupled like that -jm */ + switch (key.getAttribute().attrName) { + case CATEGORY: + return getFilesWithCategory((Category) key.getValue()); + default: + statement = getGroupStatment(key.getAttribute()); + } + + statement.setObject(1, key.getValue()); + //I hate this! not flexible/generic/maintainable -jm + //even special enums like for blackboard artifacts seems clunky +// switch (key.getAttribute().getValueType()) { +// case STRING: +// statement.setString(1, (String) key.getValue()); +// break; +// case LONG: +// statement.setLong(1, ((Integer) key.getValue()).longValue()); +// break; +// case BOOLEAN: +// statement.setInt(1, (int) key.getValue()); +// } + try (ResultSet valsResults = statement.executeQuery()) { + while (valsResults.next()) { + files.add(getFileFromID(valsResults.getLong(OBJ_ID), valsResults.getBoolean(ANALYZED))); + } + } + } catch (SQLException ex) { + LOGGER.log(Level.WARNING, "failed to get file for group:" + key.getAttribute() + " == " + key.getValue(), ex); + } finally { + dbReadUnlock(); + } + + return files; + } + + private void closeStatements() throws SQLException { + for (PreparedStatement pStmt : preparedStatements) { + pStmt.close(); + } + } + + public List<DrawableFile> getFilesWithCategory(Category cat) throws TskCoreException, IllegalArgumentException { + try { + List<DrawableFile> files = new ArrayList<>(); + List<ContentTag> contentTags = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByTagName(cat.getTagName()); + for (ContentTag ct : contentTags) { + if (ct.getContent() instanceof AbstractFile) { + files.add(DrawableFile.create((AbstractFile) ct.getContent(), isFileAnalyzed(ct.getContent().getId()))); + } + } + return files; + } catch (TskCoreException ex) { + LOGGER.log(Level.WARNING, "TSK error getting files in Category:" + cat.getDisplayName(), ex); + throw ex; + } + } + + private PreparedStatement getGroupStatment(DrawableAttribute groupBy) { + return groupStatementMap.get(groupBy); + + } + + public int countAllFiles() { + int result = -1; + dbReadLock(); + try (ResultSet rs = con.createStatement().executeQuery("select count(*) as COUNT from drawable_files")) { + while (rs.next()) { + + result = rs.getInt("COUNT"); + break; + } + } catch (SQLException ex) { + Exceptions.printStackTrace(ex); + } finally { + dbReadUnlock(); + } + return result; + } + + /** + * delete the row with obj_id = id. + * + * @param id the obj_id of the row to be deleted + * + * @return true if a row was deleted, 0 if not. + */ + public boolean removeFile(long id, DrawableTransaction tr) { + if (tr.isClosed()) { + throw new IllegalArgumentException("can't update database with closed transaction"); + } + int valsResults = 0; + dbWriteLock(); + + try { + //"delete from drawable_files where (obj_id = " + id + ")" + removeFileStmt.setLong(1, id); + removeFileStmt.executeUpdate(); + tr.addRemovedFile(id); + } catch (SQLException ex) { + LOGGER.log(Level.WARNING, "failed to delete row for obj_id = " + id, ex); + } finally { + dbWriteUnlock(); + } + + //indicates succesfull removal of 1 file + return valsResults == 1; + } + + /** + * Mark all un analyzed files as analyzed. + * + * TODO: This is a hack we only do because their is + * a bug that even after ingest is done, their are sometimes still files + * that are not marked as analyzed. Ultimately we should track down the + * underlying bug. -jm + * + * @return the ids of files that we marked as analyzed + */ + public ArrayList<Long> markAllFilesAnalyzed() { + DrawableTransaction trans = beginTransaction(); + ArrayList<Long> ids = new ArrayList<>(); + dbWriteLock(); + try (Statement statement = con.createStatement(); + ResultSet executeQuery = statement.executeQuery("select obj_id from drawable_files where analyzed = 0")) { + + while (executeQuery.next()) { + ids.add(executeQuery.getLong("obj_id")); + } + + if (ids.isEmpty() == false) { + Logger.getAnonymousLogger().log(Level.INFO, "marking as analyzed " + ids); + statement.executeUpdate("update drawable_files set analyzed = 1 where obj_id in (" + StringUtils.join(ids, ",") + ")"); + trans.updatedFiles.addAll(ids); + } + } catch (SQLException ex) { + LOGGER.log(Level.WARNING, "failed to mark files as analyzed", ex); + } finally { + dbWriteUnlock(); + } + + trans.commit(Boolean.TRUE); + + return ids; + } + + public class MultipleTransactionException extends IllegalStateException { + + private static final String CANNOT_HAVE_MORE_THAN_ONE_OPEN_TRANSACTIO = "cannot have more than one open transaction"; + + public MultipleTransactionException() { + super(CANNOT_HAVE_MORE_THAN_ONE_OPEN_TRANSACTIO); + } + } + + /** + * inner class that can reference access database connection + */ + public class DrawableTransaction { + + private final Set<Long> updatedFiles; + + private final Set<Long> removedFiles; + + private boolean closed = false; + + /** + * factory creation method + * + * @param con the {@link ava.sql.Connection} + * + * @return a LogicalFileTransaction for the given connection + * + * @throws SQLException + */ + private DrawableTransaction() { + this.updatedFiles = new HashSet<>(); + this.removedFiles = new HashSet<>(); + //get the write lock, released in close() + dbWriteLock(); + try { + con.setAutoCommit(false); + + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "failed to set auto-commit to to false", ex); + } + + } + + synchronized public void rollback() { + if (!closed) { + try { + con.rollback(); + updatedFiles.clear(); + } catch (SQLException ex1) { + LOGGER.log(Level.SEVERE, "Exception while attempting to rollback!!", ex1); + } finally { + close(); + } + } + } + + synchronized private void commit(Boolean notify) { + if (!closed) { + try { + con.commit(); + // make sure we close before we update, bc they'll need locks + close(); + + if (notify) { + fireUpdatedFiles(updatedFiles); + fireRemovedFiles(removedFiles); + } + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "Error commiting drawable.db.", ex); + rollback(); + } + } + } + + synchronized private void close() { + if (!closed) { + try { + con.setAutoCommit(true); + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "Error setting auto-commit to true.", ex); + } finally { + closed = true; + dbWriteUnlock(); + } + } + } + + synchronized public Boolean isClosed() { + return closed; + } + + synchronized private void addUpdatedFile(Long f) { + updatedFiles.add(f); + } + + synchronized private void addRemovedFile(long id) { + removedFiles.add(id); + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/DrawableFile.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/DrawableFile.java new file mode 100644 index 0000000000000000000000000000000000000000..c29d125790c839a84cc280718ada9a77e487ecd4 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/DrawableFile.java @@ -0,0 +1,370 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.datamodel; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.Node; +import javafx.scene.image.Image; +import javafx.util.Pair; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.text.WordUtils; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.imageanalyzer.EurekaModule; +import static org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute.AttributeName.WIDTH; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import static org.sleuthkit.datamodel.BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.BYTE; +import static org.sleuthkit.datamodel.BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DOUBLE; +import static org.sleuthkit.datamodel.BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.INTEGER; +import static org.sleuthkit.datamodel.BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.LONG; +import static org.sleuthkit.datamodel.BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.ContentTag; +import org.sleuthkit.datamodel.ContentVisitor; +import org.sleuthkit.datamodel.SleuthkitItemVisitor; +import org.sleuthkit.datamodel.TagName; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * @TODO: There is something I don't understand or have done wrong about + * implementing this class,as it is unreadable by + * {@link ReadContentInputStream}. As a work around I kept a reference to the + * original {@link AbstractFile} to use when reading the image. -jm + */ +public abstract class DrawableFile<T extends AbstractFile> extends AbstractFile { + + public static DrawableFile create(AbstractFile abstractFileById, boolean b) { + if (EurekaModule.isVideoFile(abstractFileById)) { + return new VideoFile(abstractFileById, b); + } else { + return new ImageFile(abstractFileById, b); + } + } + + public static DrawableFile create(Long id, boolean b) throws TskCoreException, IllegalStateException { + + AbstractFile abstractFileById = Case.getCurrentCase().getSleuthkitCase().getAbstractFileById(id); + if (EurekaModule.isVideoFile(abstractFileById)) { + return new VideoFile(abstractFileById, b); + } else { + return new ImageFile(abstractFileById, b); + } + } + + private String drawablePath; + + abstract public boolean isVideo(); + + protected T file; + + private final SimpleBooleanProperty analyzed; + + private final SimpleObjectProperty<Category> category = new SimpleObjectProperty<>(null); + + private final ObservableList<String> hashHitSetNames = FXCollections.observableArrayList(); + + public ObservableList<String> getHashHitSetNames() { + return hashHitSetNames; + } + + private String make; + + private String model; + + protected DrawableFile(T file, Boolean analyzed) { + /* @TODO: the two 'new Integer(0).shortValue()' values and null are + * placeholders because the super constructor expects values i can't get + * easily at the moment */ + + super(file.getSleuthkitCase(), file.getId(), file.getAttrType(), file.getAttrId(), file.getName(), file.getType(), file.getMetaAddr(), (int) file.getMetaSeq(), file.getDirType(), file.getMetaType(), null, new Integer(0).shortValue(), file.getSize(), file.getCtime(), file.getCrtime(), file.getAtime(), file.getMtime(), new Integer(0).shortValue(), file.getUid(), file.getGid(), file.getMd5Hash(), file.getKnown(), file.getParentPath()); + this.analyzed = new SimpleBooleanProperty(analyzed); + this.file = file; + updateHashSetHits(); + } + + @Override + public boolean isRoot() { + return false; + } + + @Override + public <T> T accept(SleuthkitItemVisitor<T> v) { + + return file.accept(v); + } + + @Override + public <T> T accept(ContentVisitor<T> v) { + return file.accept(v); + } + + @Override + public List<Content> getChildren() throws TskCoreException { + return new ArrayList<>(); + } + + @Override + public List<Long> getChildrenIds() throws TskCoreException { + return new ArrayList<>(); + } + + public ObservableList<Pair<DrawableAttribute, ? extends Object>> getAttributesList() { + final ObservableList<Pair<DrawableAttribute, ? extends Object>> attributeList = FXCollections.observableArrayList(); + for (DrawableAttribute attr : DrawableAttribute.getValues()) { + attributeList.add(new Pair<>(attr, getValueOfAttribute(attr))); + } + return attributeList; + } + + public Object getValueOfAttribute(DrawableAttribute attr) { + switch (attr.attrName) { + case OBJ_ID: + return file.getId(); + case PATH: + return getDrawablePath(); + case NAME: + return getName(); + case CREATED_TIME: + return getCrtimeAsDate(); + case MODIFIED_TIME: + return getMtimeAsDate(); + case MAKE: + if (make == null) { + make = WordUtils.capitalizeFully((String) getValueOfBBAttribute(BlackboardArtifact.ARTIFACT_TYPE.TSK_METADATA_EXIF, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_MAKE)); + } + return make; + case MODEL: + if (model == null) { + model = WordUtils.capitalizeFully((String) getValueOfBBAttribute(BlackboardArtifact.ARTIFACT_TYPE.TSK_METADATA_EXIF, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_MODEL)); + } + return model; + + case CATEGORY: + + updateCategory(); + + return category.get(); + + case TAGS: + try { + List<ContentTag> contentTagsByContent = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByContent(this); + Set<TagName> values = new HashSet<>(); + + for (ContentTag ct : contentTagsByContent) { + values.add(ct.getName()); + } + return new ArrayList<>(values); + + } catch (TskCoreException ex) { + Logger.getAnonymousLogger().log(Level.WARNING, "problem looking up " + attr.getDisplayName() + " for " + file.getName(), ex); + return new ArrayList<>(); + } + + case ANALYZED: + return analyzed.get(); + case HASHSET: + updateHashSetHits(); + return hashHitSetNames; + case WIDTH: + return getWidth(); + case HEIGHT: + return getHeight(); + + default: + throw new UnsupportedOperationException("DrawableFile.getValueOfAttribute does not yet support " + attr.getDisplayName()); + + } + } + + public List<? extends Object> getValuesOfBBAttribute(BlackboardArtifact.ARTIFACT_TYPE artType, BlackboardAttribute.ATTRIBUTE_TYPE attrType) { + ArrayList<Object> vals = new ArrayList<>(); + try { + //why doesn't file.getArtifacts() work? + //TODO: this seams like overkill, use a more targeted query + ArrayList<BlackboardArtifact> artifacts = getAllArtifacts(); + + for (BlackboardArtifact artf : artifacts) { + if (artf.getArtifactTypeID() == artType.getTypeID()) { + for (BlackboardAttribute attr : artf.getAttributes()) { + if (attr.getAttributeTypeID() == attrType.getTypeID()) { + + switch (attr.getValueType()) { + case BYTE: + vals.add(attr.getValueBytes()); + case DOUBLE: + vals.add(attr.getValueDouble()); + case INTEGER: + vals.add(attr.getValueInt()); + case LONG: + vals.add(attr.getValueLong()); + case STRING: + vals.add(attr.getValueString()); + } + } + } + } + } + } catch (TskCoreException ex) { + Logger.getAnonymousLogger().log(Level.WARNING, "problem looking up {0}/{1}" + " " + " for {2}", new Object[]{artType.getDisplayName(), attrType.getDisplayName(), getName()}); + } + + return vals; + } + + private Object getValueOfBBAttribute(BlackboardArtifact.ARTIFACT_TYPE artType, BlackboardAttribute.ATTRIBUTE_TYPE attrType) { + try { + + //why doesn't file.getArtifacts() work? + //TODO: this seams like overkill, use a more targeted query + ArrayList<BlackboardArtifact> artifacts = file.getArtifacts(artType);// getAllArtifacts(); + + for (BlackboardArtifact artf : artifacts) { + if (artf.getArtifactTypeID() == artType.getTypeID()) { + for (BlackboardAttribute attr : artf.getAttributes()) { + if (attr.getAttributeTypeID() == attrType.getTypeID()) { + switch (attr.getValueType()) { + case BYTE: + return attr.getValueBytes(); + case DOUBLE: + return attr.getValueDouble(); + case INTEGER: + return attr.getValueInt(); + case LONG: + return attr.getValueLong(); + case STRING: + return attr.getValueString(); + } + } + } + } + } + } catch (TskCoreException ex) { + Logger.getAnonymousLogger().log(Level.WARNING, "problem looking up {0}/{1}" + " " + " for {2}", new Object[]{artType.getDisplayName(), attrType.getDisplayName(), getName()}); + } + return ""; + } + + public String getMake() { + return getValueOfAttribute(DrawableAttribute.MAKE).toString(); + } + + public String getModel() { + return getValueOfAttribute(DrawableAttribute.MODEL).toString(); + } + + public void setCategory(Category category) { + categoryProperty().set(category); + + } + + public Category getCategory() { + return (Category) getValueOfAttribute(DrawableAttribute.CATEGORY); + } + + public SimpleObjectProperty<Category> categoryProperty() { + return category; + } + + public void updateCategory() { + try { + List<ContentTag> contentTagsByContent = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByContent(this); + Category cat = null; + for (ContentTag ct : contentTagsByContent) { + if (ct.getName().getDisplayName().startsWith(Category.CATEGORY_PREFIX)) { + cat = Category.fromDisplayName(ct.getName().getDisplayName()); + break; + } + } + if (cat == null) { + category.set(Category.ZERO); + } else { + category.set(cat); + } + } catch (TskCoreException ex) { + Logger.getLogger(DrawableFile.class.getName()).log(Level.WARNING, "problem looking up category for file " + this.getName(), ex); + } + } + + public abstract Node getFullsizeDisplayNode(); + + public abstract Image getIcon(); + + public void setAnalyzed(Boolean analyzed) { + this.analyzed.set(analyzed); + } + + public boolean isAnalyzed() { + return analyzed.get(); + } + + public T getAbstractFile() { + return this.file; + } + + private void updateHashSetHits() { + hashHitSetNames.setAll((Collection<? extends String>) getValuesOfBBAttribute(BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME)); + } + + abstract Number getWidth(); + + abstract Number getHeight(); + + private static final String[] SLASH = new String[]{"/"}; + + private static final String[] DOUBLE_SLASH = new String[]{"//"}; + + public String getDrawablePath() { + if (drawablePath != null) { + return drawablePath; + } else { + try { + drawablePath = StringUtils.removeEnd(getUniquePath(), getName()); +// drawablePath = StringUtils.replaceEachRepeatedly(drawablePath, DOUBLE_SLASH, SLASH); + return drawablePath; + } catch (TskCoreException ex) { + Logger.getLogger(DrawableFile.class.getName()).log(Level.WARNING, "failed to get drawablePath from " + getName()); + return ""; + } + } + } + + private long getRootID() throws TskCoreException { + + Content myParent = getParent(); + long id = -1; + + while (myParent != null) { + id = myParent.getId(); + myParent = myParent.getParent(); + } + + return id; + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/ImageFile.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/ImageFile.java new file mode 100644 index 0000000000000000000000000000000000000000..70c0c050521e5852c61bbacd08ecc39f2df7aeac --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/ImageFile.java @@ -0,0 +1,118 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.datamodel; + +import org.sleuthkit.autopsy.imageanalyzer.IconCache; +import org.sleuthkit.autopsy.imageanalyzer.gui.Fitable; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.lang.ref.SoftReference; +import java.util.logging.Level; +import javafx.embed.swing.SwingFXUtils; +import javafx.scene.Node; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javax.imageio.ImageIO; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.ReadContentInputStream; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Eureka data model object that represents an image file. It is a + * wrapper(/decorator?/adapter?) around {@link AbstractFile} and provides + * methods to get an icon sized and a full sized {@link Image}. + * + * + */ +public class ImageFile extends DrawableFile { + + private SoftReference<Image> imageRef; + + ImageFile(AbstractFile f, Boolean analyzed) { + super(f, analyzed); + + } + + ImageFile(Long id, Boolean analyzed) throws TskCoreException { + super(Case.getCurrentCase().getSleuthkitCase().getAbstractFileById(id), analyzed); + } + + @Override + public Image getIcon() { + return IconCache.getDefault().get(this); + } + + @Override + public Node getFullsizeDisplayNode() { + return new FitableImageView(getFullSizeImage()); + } + + public Image getFullSizeImage() { + Image image = null; + if (imageRef != null) { + image = imageRef.get(); + } + + if (image == null) { + + try (ReadContentInputStream readContentInputStream = new ReadContentInputStream(this.getAbstractFile())) { + BufferedImage read = ImageIO.read(readContentInputStream); + image = SwingFXUtils.toFXImage(read, null); + } catch (IOException | NullPointerException ex) { + Logger.getLogger(ImageFile.class.getName()).log(Level.WARNING, "unable to read file" + getName()); + return null; + } + imageRef = new SoftReference<>(image); + } + return image; + } + + @Override + Number getWidth() { + final Image fullSizeImage = getFullSizeImage(); + if (fullSizeImage != null) { + return fullSizeImage.getWidth(); + } + return -1; + } + + @Override + Number getHeight() { + final Image fullSizeImage = getFullSizeImage(); + if (fullSizeImage != null) { + return fullSizeImage.getHeight(); + } + return -1; + } + + @Override + public boolean isVideo() { + return false; + } + + private class FitableImageView extends ImageView implements Fitable { + + public FitableImageView(Image image) { + super(image); + } + + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/VideoFile.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/VideoFile.java new file mode 100644 index 0000000000000000000000000000000000000000..2bb16fe5b3ca334b18569e04b971ff420164a44c --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/datamodel/VideoFile.java @@ -0,0 +1,132 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.datamodel; + +import org.sleuthkit.autopsy.imageanalyzer.gui.MediaControl; +import java.io.File; +import java.io.IOException; +import java.lang.ref.SoftReference; +import java.nio.file.Paths; +import java.util.logging.Level; +import javafx.scene.Node; +import javafx.scene.image.Image; +import javafx.scene.media.Media; +import javafx.scene.media.MediaException; +import javafx.scene.media.MediaPlayer; +import javafx.scene.text.Text; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.datamodel.ContentUtils; +import org.sleuthkit.datamodel.AbstractFile; + +public class VideoFile extends DrawableFile { + + private static final Image VIDEO_ICON = new Image("org/sleuthkit/autopsy/imageanalyzer/images/Clapperboard.png"); + + VideoFile(AbstractFile file, Boolean analyzed) { + super(file, analyzed); + } + + @Override + public Image getIcon() { + //TODO: implement video thumbnailing here + return VIDEO_ICON; + } + SoftReference<Media> mediaRef; + + public Media getMedia() throws IOException, MediaException { + Media media = null; + if (mediaRef != null) { + media = mediaRef.get(); + } + if (media != null) { + return media; + } + final File cacheFile = getCacheFile(this.getId()); + if (cacheFile.exists() == false) { + + ContentUtils.writeToFile(this.getAbstractFile(), cacheFile); + + } + try { + media = new Media(Paths.get(cacheFile.getAbsolutePath()).toUri().toString()); + mediaRef = new SoftReference<>(media); + return media; + } catch (MediaException ex) { + throw ex; + } + } + + @Override + public Node getFullsizeDisplayNode() { + final File cacheFile = getCacheFile(this.getId()); + + try { + if (cacheFile.exists() == false) { + ContentUtils.writeToFile(this.getAbstractFile(), cacheFile); + } + final Media media = getMedia(); + final MediaPlayer mediaPlayer = new MediaPlayer(media); + final MediaControl mediaView = new MediaControl(mediaPlayer); + mediaPlayer.statusProperty().addListener((observableStatus, oldStatus, newStatus) -> { + Logger.getAnonymousLogger().log(Level.INFO, "media player: {0}", newStatus); + }); + return mediaView; + } catch (IOException ex) { + Logger.getLogger(VideoFile.class.getName()).log(Level.SEVERE, "failed to initialize MediaControl for file " + getName(), ex); + return new Text(ex.getLocalizedMessage() + "\nSee the logs for details."); + } catch (MediaException ex) { + Logger.getLogger(VideoFile.class.getName()).log(Level.SEVERE, ex.getType() + " Failed to initialize MediaControl for file " + getName(), ex); + Logger.getLogger(VideoFile.class.getName()).log(Level.SEVERE, "caused by " + ex.getCause().getLocalizedMessage(), ex.getCause()); + return new Text(ex.getType() + "\nSee the logs for details."); + } catch (OutOfMemoryError ex) { + Logger.getLogger(VideoFile.class.getName()).log(Level.SEVERE, "failed to initialize MediaControl for file " + getName(), ex); + return new Text("There was a problem playing video file.\nSee the logs for details."); + } + } + + private File getCacheFile(long id) { + return new File(Case.getCurrentCase().getCacheDirectory() + File.separator + id); + } + + @Override + Number getWidth() { + try { + return getMedia().getWidth(); + } catch (IOException | MediaException ex) { +// Exceptions.printStackTrace(ex); + return -1; + } + } + + @Override + public boolean isVideo() { + return true; + } + + @Override + Number getHeight() { + try { + return getMedia().getHeight(); + } catch (IOException | MediaException ex) { +// Exceptions.printStackTrace(ex); + return -1; + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FilterPane.css b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FilterPane.css new file mode 100644 index 0000000000000000000000000000000000000000..937a27b23e50a40cf9841074b4b02b477aedf6f2 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FilterPane.css @@ -0,0 +1,6 @@ +.list-cell:odd { + -fx-background-color: transparent; /* derive(-fx-control-inner-background,-5%); */ +} +.list-cell:even { + -fx-background-color: transparent; /* derive(-fx-control-inner-background,-5%); */ +} \ No newline at end of file diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FilterPane.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FilterPane.fxml new file mode 100644 index 0000000000000000000000000000000000000000..b9e198f27458b23c6d57e87679cae828b537963e --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FilterPane.fxml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import java.lang.*?> +<?import java.net.*?> +<?import java.util.*?> +<?import javafx.geometry.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.image.*?> +<?import javafx.scene.layout.*?> +<?import javafx.scene.paint.*?> + +<fx:root type="javafx.scene.control.TitledPane" alignment="TOP_LEFT" maxHeight="-Infinity" maxWidth="1.7976931348623157E308" minWidth="-1.0" text="filter label" wrapText="true" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2"> + <content> + <VBox fx:id="filtersBox" prefHeight="-1.0" prefWidth="-1.0" /> + </content> + <graphic> + <HBox alignment="CENTER_LEFT"> + <children> + <CheckBox fx:id="selectedBox" mnemonicParsing="false" selected="true" /> + </children> + <padding> + <Insets left="5.0" /> + </padding> + </HBox> + </graphic> +</fx:root> diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FilterPane.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FilterPane.java new file mode 100644 index 0000000000000000000000000000000000000000..d2294c700d08827e6093b088beadad22a00bf895 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FilterPane.java @@ -0,0 +1,99 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.filtering; + +import org.sleuthkit.autopsy.imageanalyzer.filtering.filters.FilterRow; +import org.sleuthkit.autopsy.imageanalyzer.filtering.filters.AtomicFilter; +import org.sleuthkit.autopsy.imageanalyzer.filtering.filters.UnionFilter; +import org.sleuthkit.autopsy.imageanalyzer.FXMLConstructor; +import java.net.URL; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.TreeMap; +import javafx.collections.ListChangeListener; +import javafx.fxml.FXML; +import javafx.scene.control.CheckBox; +import javafx.scene.control.TitledPane; +import javafx.scene.layout.VBox; + +/** + * FXML Controller class + * + */ +public class FilterPane extends TitledPane { + + @FXML + private ResourceBundle resources; + @FXML + private URL location; + @FXML + private VBox filtersBox; + @FXML + private CheckBox selectedBox; + private UnionFilter<AtomicFilter> filter; + private Map<AtomicFilter, FilterRow> filterRowMap = new TreeMap<>(AtomicFilter.ALPHABETIC_COMPARATOR); + + @FXML + void initialize() { + assert filtersBox != null : "fx:id=\"filtersBox\" was not injected: check your FXML file 'FilterPane.fxml'."; + assert selectedBox != null : "fx:id=\"selectedBox\" was not injected: check your FXML file 'FilterPane.fxml'."; + } + + public FilterPane() { + FXMLConstructor.construct(this, "FilterPane.fxml"); + } + + private void rebuildChildren() { + filtersBox.getChildren().clear(); + for (FilterRow af : filterRowMap.values()) { + filtersBox.getChildren().add(af); + } + } + + void setFilter(UnionFilter<AtomicFilter> filter) { +//TODO : do this more reasonably + this.filter = filter; + this.setText(filter.getDisplayName()); + filterRowMap.clear(); + filtersBox.getChildren().clear(); + for (AtomicFilter af : filter.subFilters) { + final FilterRow filterRow = af.getUI(); + filterRowMap.put(af, filterRow); + } + rebuildChildren(); + + this.filter.subFilters.addListener(new ListChangeListener<AtomicFilter>() { + @Override + public void onChanged(ListChangeListener.Change<? extends AtomicFilter> change) { + while (change.next()) { + for (AtomicFilter af : change.getAddedSubList()) { + FilterRow filterRow = af.getUI(); + filterRowMap.put(af, filterRow); + } + for (AtomicFilter af : change.getRemoved()) { + filterRowMap.remove(af); + } + } + rebuildChildren(); + } + }); + + this.filter.active.bindBidirectional(selectedBox.selectedProperty()); + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FiltersPanel.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FiltersPanel.fxml new file mode 100644 index 0000000000000000000000000000000000000000..41636f42d7e8414fda90926aa68c529855737048 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FiltersPanel.fxml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import java.lang.*?> +<?import java.util.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.image.*?> +<?import javafx.scene.layout.*?> +<?import javafx.scene.paint.*?> + +<fx:root type="javafx.scene.layout.AnchorPane" id="AnchorPane" maxHeight="1.7976931348623157E308" maxWidth="-1.0" minHeight="-1.0" minWidth="200.0" prefHeight="-1.0" prefWidth="-1.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2"> + <children> + <TabPane maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" prefHeight="-1.0" prefWidth="-1.0" rotateGraphic="true" side="TOP" tabClosingPolicy="UNAVAILABLE" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"> + <tabs> + <Tab closable="false" text="Filters"> + <content> + <ListView fx:id="filtersList" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" prefHeight="-1.0" prefWidth="-1.0" /> + </content> + <graphic> + <ImageView fitHeight="16.0" fitWidth="16.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true"> + <image> + <Image url="@../images/funnel.png" /> + </image> + </ImageView> + </graphic> + </Tab> + <Tab closable="false" disable="true" text="File Tree"> + <content> + <AnchorPane id="Content" minHeight="0.0" minWidth="0.0" prefHeight="-1.0" prefWidth="-1.0" /> + </content> + </Tab> + </tabs> + </TabPane> + </children> +</fx:root> diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FiltersPanel.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FiltersPanel.java new file mode 100644 index 0000000000000000000000000000000000000000..2281841d21ffef236ce311738627d4bbe15dc9c4 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/FiltersPanel.java @@ -0,0 +1,353 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.filtering; + +import org.sleuthkit.autopsy.imageanalyzer.EurekaController; +import org.sleuthkit.autopsy.imageanalyzer.filtering.filters.FilterSet; +import org.sleuthkit.autopsy.imageanalyzer.filtering.filters.AtomicFilter; +import org.sleuthkit.autopsy.imageanalyzer.filtering.filters.AttributeFilter; +import org.sleuthkit.autopsy.imageanalyzer.EurekaModule; +import org.sleuthkit.autopsy.imageanalyzer.LoggedTask; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute; +import org.sleuthkit.autopsy.imageanalyzer.FXMLConstructor; +import org.sleuthkit.autopsy.imageanalyzer.FileUpdateEvent; +import org.sleuthkit.autopsy.imageanalyzer.FileUpdateListener; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableDB; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile; +import org.sleuthkit.autopsy.imageanalyzer.filtering.filters.AbstractFilter; +import org.sleuthkit.autopsy.imageanalyzer.filtering.filters.NameFilter; +import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupManager; +import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupSortBy; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.net.URL; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.logging.Level; +import javafx.application.Platform; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.concurrent.Service; +import javafx.concurrent.Task; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.AnchorPane; +import javafx.util.Callback; +import javax.swing.SortOrder; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.imageanalyzer.ThreadUtils; + +/** + * This singleton acts as the controller for the Filters. It creates filters + * based on values in the database, and broadcasts events when the activation + * state or other ui configuration of the filters changes. + * + * deprecated until we revisit filtering + */ +@Deprecated +public class FiltersPanel extends AnchorPane implements FileUpdateListener { + + public static final String FILTER_STATE_CHANGED = "FILTER_STATE_CHANGED"; + @FXML + private ResourceBundle resources; + @FXML + private URL location; + @FXML + private ListView< AttributeFilter> filtersList; + private static final Logger LOGGER = Logger.getLogger(FiltersPanel.class.getName()); + volatile private DrawableDB db; + final Map<DrawableAttribute, AttributeFilter> attrFilterMap = new HashMap<>(); + private static FiltersPanel instance; + + /** + * clear/reset state + */ + public void clear() { + Platform.runLater(new Runnable() { + @Override + public void run() { + db = null; + filterSet.clear(); + attrFilterMap.clear(); + } + }); + } + /** + * listen to changes in individual filters and forward to external listeners + * via the pcs + */ + private final ChangeListener filterForwardingListener = new ChangeListener<Object>() { + @Override + public void changed(ObservableValue<? extends Object> observable, Object oldValue, Object newValue) { + pcs.firePropertyChange(new PropertyChangeEvent(observable, FILTER_STATE_CHANGED, oldValue, newValue)); + } + }; + + public FilterSet getFilterSet() { + return filterSet; + } + /* top level filter */ + private FilterSet filterSet = new FilterSet(); + /** + * {@link Service} to (re)build filterset based on values in the database + */ + private final RebuildFiltersService rebuildFiltersService = new RebuildFiltersService(); + private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); + + /** + * register a {@link PropertyChangeListener} + * + * @param pcl + */ + public void addListener(PropertyChangeListener pcl) { + pcs.addPropertyChangeListener(pcl); + } + + @FXML + void initialize() { + assert filtersList != null : "fx:id=\"filtersList\" was not injected: check your FXML file 'FiltersPanel.fxml'."; + + filtersList.setItems(filterSet.subFilters); + + filtersList.setCellFactory(new Callback<ListView<AttributeFilter>, ListCell<AttributeFilter>>() { + @Override + public ListCell<AttributeFilter> call(ListView<AttributeFilter> p) { + return new FilterPaneListCell(); + } + }); + for (final DrawableAttribute attr : DrawableAttribute.getValues()) { + if (attr != DrawableAttribute.NAME && attr != DrawableAttribute.CREATED_TIME && attr != DrawableAttribute.MODIFIED_TIME) { + final AttributeFilter attrFilter = new AttributeFilter(attr); + filterSet.subFilters.add(attrFilter); + attrFilterMap.put(attr, attrFilter); + } + } + } + + static synchronized public FiltersPanel getDefault() { + if (instance == null) { + instance = new FiltersPanel(); + } + return instance; + } + + private FiltersPanel() { + + FXMLConstructor.construct(this, "FiltersPanel.fxml"); + } + + public void setDB(DrawableDB drawableDb) { + db = drawableDb; + db.addUpdatedFileListener(this); + rebuildFilters(); + } + + @Override + synchronized public void handleFileUpdate(FileUpdateEvent evt) { + //updateFilters(evt.getUpdatedFiles()); + } + + synchronized public void rebuildFilters() { + /* + * Platform.runLater(new Runnable() { + * @Override + * public void run() { + * rebuildFiltersService.restart(); + * } + * }); + * */ + } + + synchronized public void updateFilters(final Collection< DrawableFile> files) { + /* + * for (DrawableFile file : files) { + * for (final DrawableAttribute attr : DrawableAttribute.getValues()) { + * AttributeFilter attributeFilter; + * AbstractFilter.FilterComparison comparison = AtomicFilter.EQUALS; + * switch (attr.attrName) { + * case NAME: + * case PATH: + * case CREATED_TIME: + * case MODIFIED_TIME: + * case CATEGORY: + * case OBJ_ID: + * case ANALYZED: + * //fall through all attributes that don't have per value filters + * break; + * case HASHSET: + * comparison = AtomicFilter.CONTAINED_IN; + * break; + * default: + * //default is make one == filter for each value in database + * attributeFilter = getFilterForAttr(attr); + * + * ObservableList<Object> vals = + * FXCollections.singletonObservableList(file.getValueOfAttribute(attr)); + * + * addFilterForAttrValues(vals, attributeFilter, comparison); + * } + * } + * } + */ + } + + synchronized private AttributeFilter getFilterForAttr(final DrawableAttribute attr) { + AttributeFilter attributeFilter = attrFilterMap.get(attr); + if (attributeFilter == null) { + attributeFilter = new AttributeFilter(attr); + attributeFilter.active.addListener(filterForwardingListener); + } + + final AttributeFilter finalFilter = attributeFilter; + +// Platform.runLater(new Runnable() { +// @Override +// public void run() { + if (filterSet.subFilters.contains(finalFilter) == false) { + filterSet.subFilters.add(finalFilter); + } +// } +// }); + + attrFilterMap.put(attr, attributeFilter); + return attributeFilter; + } + + synchronized private void addFilterForAttrValues(List<? extends Object> vals, final AttributeFilter attributeFilter, final AbstractFilter.FilterComparison filterComparison) { + for (final Object val : vals) { + if (attributeFilter.containsSubFilterForValue(val) == false) { + final AtomicFilter filter = new AtomicFilter(attributeFilter.getAttribute(), filterComparison, val); + Platform.runLater(new Runnable() { + @Override + public void run() { + attributeFilter.subFilters.add(filter); + } + }); + + filter.active.addListener(filterForwardingListener); +// Logger.getAnonymousLogger().log(Level.INFO, "created filter " + filter); + } + + } + } + + /** + * + */ + static class FilterPaneListCell extends ListCell<AttributeFilter> { + + private final FilterPane filterPane; + + public FilterPaneListCell() { + super(); + filterPane = new FilterPane(); + } + + @Override + protected void updateItem(final AttributeFilter item, final boolean empty) { + super.updateItem(item, empty); + Platform.runLater(new Runnable() { + @Override + public void run() { + if (empty || item == null) { + setGraphic(null); + } else { + setGraphic(filterPane); + filterPane.setFilter(item); + } + } + }); + + + } + } + + private class RebuildFiltersService extends Service<Void> { + + @Override + protected Task<Void> createTask() { + return new RebuildFiltersTask(); + } + + private class RebuildFiltersTask extends LoggedTask<Void> { + + public RebuildFiltersTask() { + super("rebuilding filters", true); + } + + @Override + protected Void call() throws Exception { + ThreadUtils.runAndWait(new Runnable() { + @Override + public void run() { + filterSet.subFilters.clear(); + attrFilterMap.clear(); + } + }); + + LOGGER.log(Level.INFO, "rebuilding filters started"); + + for (final DrawableAttribute attr : DrawableAttribute.getValues()) { + AbstractFilter.FilterComparison comparison = AtomicFilter.EQUALS; + switch (attr.attrName) { + case NAME: + final AttributeFilter nameFilter = getFilterForAttr(attr); + final NameFilter filter = new NameFilter(); + Platform.runLater(new Runnable() { + @Override + public void run() { + nameFilter.subFilters.add(filter); + } + }); + + filter.active.addListener(filterForwardingListener); + filter.filterValue.addListener(filterForwardingListener); + + LOGGER.log(Level.INFO, "createdfilter {0}", filter); + break; + + case PATH: + case CREATED_TIME: + case MODIFIED_TIME: + case OBJ_ID: + break; + case HASHSET: + comparison = AtomicFilter.CONTAINED_IN; + break; + default: + + //default is make one == filter per attribute value in db + final AttributeFilter attributeFilter = getFilterForAttr(attr); + //TODO: FILE_COUNT is arbitrarty but maybe better than NONE, we can include file counts in labels in future + List<? extends Object> vals = EurekaController.getDefault().getGroupManager().findValuesForAttribute(attr, GroupSortBy.FILE_COUNT, SortOrder.DESCENDING); + addFilterForAttrValues(vals, attributeFilter, comparison); + } + } + return null; + } + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/AbstractFilter.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/AbstractFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..c8bf253293f884d4f75834d3c5dd3e64eecd9dc9 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/AbstractFilter.java @@ -0,0 +1,131 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.sleuthkit.autopsy.imageanalyzer.filtering.filters; + +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile; +import java.util.List; +import javafx.beans.property.SimpleBooleanProperty; + +/** + * + * @author jonathan + */ +public abstract class AbstractFilter<R> { + + public abstract void clear(); + public static final AtomicFilter.FilterComparison EQUALS = new AtomicFilter.EQUALS(); + public static final AtomicFilter.FilterComparison EQUALS_IGNORECASE = new AtomicFilter.EQUALS_IGNORECASE(); + public static final AtomicFilter.FilterComparison NOT_EQUALS = new AtomicFilter.NOT_EQUALS(); + public static final AtomicFilter.FilterComparison SUBSTRING = new AtomicFilter.SUBSTRING(); + public static final AtomicFilter.FilterComparison CONTAINED_IN = new AtomicFilter.CONTAINS(); + public final SimpleBooleanProperty active = new SimpleBooleanProperty(true); + + public abstract String getDisplayName(); + + abstract public Boolean accept(DrawableFile df); + + public boolean isActive() { + return active.get(); + } + + final protected static class CONTAINS extends AtomicFilter.FilterComparison<List<String>, String> { + + @Override + public String getSqlOperator() { + return " in "; + } + + @Override + public Boolean compare(List<String> attrVal, String filterVal) { + return attrVal.contains(filterVal); + } + } + + final protected static class SUBSTRING extends AtomicFilter.FilterComparison<String, String> { + + @Override + public Boolean compare(String attrVal, String filterVal) { + return attrVal.toLowerCase().contains(filterVal.toLowerCase()); + } + + @Override + public String getSqlOperator() { + return " like "; + } + + @Override + public String toString() { + return "in"; + } + } + + final protected static class EQUALS_IGNORECASE extends AtomicFilter.FilterComparison<String, String> { + + @Override + public Boolean compare(String attrVal, String filterVal) { + return attrVal.equals(filterVal); + } + + @Override + public String getSqlOperator() { + return " == "; + } + + @Override + public String toString() { + return "="; + } + } + + final protected static class EQUALS<T> extends AtomicFilter.FilterComparison<T, T> { + + @Override + public Boolean compare(T attrVal, T filterVal) { + return attrVal.equals(filterVal); + } + + @Override + public String getSqlOperator() { + return " == "; + } + + @Override + public String toString() { + return "="; + } + } + + final protected static class NOT_EQUALS<T> extends AtomicFilter.FilterComparison<T, T> { + + @Override + public Boolean compare(T attrVal, T filterVal) { + return attrVal != filterVal; + } + + @Override + public String getSqlOperator() { + return " != "; + } + + @Override + public String toString() { + return "≠"; + } + } + + /** + * + * + */ + public static abstract class FilterComparison<A, F> { + + private FilterComparison() { + } + + abstract public Boolean compare(A attrVal, F filterVal); + + abstract public String getSqlOperator(); + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/AtomicFilter.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/AtomicFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..77c545e8952fcdfeaf9e21c6cfd5c8f07cf05bba --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/AtomicFilter.java @@ -0,0 +1,168 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.sleuthkit.autopsy.imageanalyzer.filtering.filters; + +import org.sleuthkit.autopsy.imageanalyzer.FXMLConstructor; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile; +import java.net.URL; +import java.util.Comparator; +import java.util.Objects; +import java.util.ResourceBundle; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.fxml.FXML; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ChoiceBox; +import javafx.scene.control.Label; + +/** + * + */ +public class AtomicFilter<A, F> extends AbstractFilter { + + private DrawableAttribute<A> filterAttribute; + + public DrawableAttribute<A> getFilterAttribute() { + return filterAttribute; + } + + public F getFilterValue() { + return filterValue.get(); + } + + public FilterComparison<A, F> getFilterComparisson() { + return filterComparisson; + } + public SimpleObjectProperty<F> filterValue; + private FilterComparison<A, F> filterComparisson; + + public AtomicFilter(DrawableAttribute filterAttribute, FilterComparison<A, F> filterComparisson, F filterValue) { + this.filterAttribute = filterAttribute; + this.filterValue = new SimpleObjectProperty<>(filterValue); + this.filterComparisson = filterComparisson; + } + + @Override + public Boolean accept(DrawableFile df) { +// Logger.getAnonymousLogger().log(Level.INFO, getDisplayName() + " : " + filterValue + " filtered " + df.getName() + " = " + compare); + return isActive() + ? filterComparisson.compare((A) df.getValueOfAttribute(filterAttribute), filterValue.get()) + : false; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 79 * hash + Objects.hashCode(this.filterAttribute); + hash = 79 * hash + Objects.hashCode(this.filterValue); + hash = 79 * hash + Objects.hashCode(this.filterComparisson); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final AtomicFilter other = (AtomicFilter) obj; + if (this.filterAttribute != other.filterAttribute) { + return false; + } + if (!Objects.equals(this.filterValue, other.filterValue)) { + return false; + } + if (!Objects.equals(this.filterComparisson, other.filterComparisson)) { + return false; + } + return true; + } + + @Override + public String getDisplayName() { + return filterValue.get().toString(); + } + + public FilterRow getUI() { + return new AtomicFilterRow(this); + } + + @Override + public void clear() { + active.set(true); + } + public static final Comparator<AtomicFilter> ALPHABETIC_COMPARATOR = new Comparator<AtomicFilter>() { + @Override + public int compare(AtomicFilter o1, AtomicFilter o2) { + return o1.getDisplayName().compareTo(o2.getDisplayName()); + } + }; + + /** + * + * @author jonathan + */ + public static class AtomicFilterRow<A, F> extends FilterRow<AtomicFilter<A, F>> { + + @FXML + protected ResourceBundle resources; + @FXML + protected URL location; + @FXML + protected ChoiceBox comparisonBox; + @FXML + protected Label filterLabel; + @FXML + protected CheckBox selectedBox; + protected final AtomicFilter<A, F> filter; + + private AtomicFilterRow(AtomicFilter filter) { + super(); + this.filter = filter; + FXMLConstructor.construct(this, "FilterRow.fxml"); + } + + public ReadOnlyBooleanProperty getSelectedProperty() { + return selectedBox.selectedProperty(); + } + + public ReadOnlyObjectProperty<AtomicFilter.FilterComparison> getComparisonProperty() { + return comparisonBox.getSelectionModel().selectedItemProperty(); + } + + @FXML + void initialize() { + assert comparisonBox != null : "fx:id=\"comparisonBox\" was not injected: check your FXML file 'FilterRow.fxml'."; + assert filterLabel != null : "fx:id=\"filterLabel\" was not injected: check your FXML file 'FilterRow.fxml'."; + assert selectedBox != null : "fx:id=\"selectedBox\" was not injected: check your FXML file 'FilterRow.fxml'."; + comparisonBox.getItems().setAll(AtomicFilter.EQUALS, AtomicFilter.EQUALS_IGNORECASE); + switch (filter.filterAttribute.attrName) { + case MAKE: + case MODEL: + comparisonBox.getSelectionModel().select(AtomicFilter.EQUALS_IGNORECASE); + break; + default: + comparisonBox.getSelectionModel().select(AtomicFilter.EQUALS); + } + final F filterValue = filter.getFilterValue(); + + if (filterValue == null || "".equals(filterValue)) { + filterLabel.setText("unknown"); + } else { + filterLabel.setText(filterValue.toString()); + } + selectedBox.selectedProperty().bindBidirectional(filter.active); + } + + @Override + public AtomicFilter<A, F> getFilter() { + return filter; + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/AtomicSqlFilter.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/AtomicSqlFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..7dff206d9d98a891e6eba99df1761f11da2458ca --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/AtomicSqlFilter.java @@ -0,0 +1,19 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.sleuthkit.autopsy.imageanalyzer.filtering.filters; + +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute; + +public class AtomicSqlFilter extends AtomicFilter implements SqlFilter { + + public AtomicSqlFilter(DrawableAttribute filterAttribute, FilterComparison filterComparisson, Object filterValue) { + super(filterAttribute, filterComparisson, filterValue); + } + + @Override + public String getFilterQueryString() { + return getFilterAttribute().attrName.name() + " " + getFilterComparisson().getSqlOperator() + " " + getFilterValue().toString(); + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/AttributeFilter.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/AttributeFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..17534ea55b000420ae4ca930b673dcdda46d055e --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/AttributeFilter.java @@ -0,0 +1,43 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.sleuthkit.autopsy.imageanalyzer.filtering.filters; + +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute; + +/** + * + * @author jonathan + */ +public class AttributeFilter<AVT, FVT> extends UnionFilter<AtomicFilter<AVT, FVT>> { + + DrawableAttribute filterAttribute; + + public AttributeFilter(DrawableAttribute filterAttribute) { + super(); + this.filterAttribute = filterAttribute; + } + + @Override + public String getDisplayName() { + return filterAttribute.getDisplayName(); + } + + public DrawableAttribute getAttribute() { + return filterAttribute; + } + + public boolean containsSubFilterForValue(FVT val) { + try { + for (AtomicFilter sf : subFilters) { + if (val.equals(sf.getFilterValue())) { + return true; + } + } + } catch (Exception e) { + } + + return false; + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/CompoundFilter.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/CompoundFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..2c1700747637f021bf72282e67d598d2b78af2fd --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/CompoundFilter.java @@ -0,0 +1,28 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.sleuthkit.autopsy.imageanalyzer.filtering.filters; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +/** + * + */ +public abstract class CompoundFilter<T extends AbstractFilter> extends AbstractFilter { + + public ObservableList<T> subFilters; + + public CompoundFilter(ObservableList< T> subFilters) { + this.subFilters = FXCollections.synchronizedObservableList(subFilters); + } + + public void clear() { + for (T filter : subFilters) { + filter.clear(); + } + active.set(true); + subFilters.clear(); + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/FilterRow.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/FilterRow.fxml new file mode 100644 index 0000000000000000000000000000000000000000..9d84e288f3380030223dad9645e0e4f6a06e93d2 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/FilterRow.fxml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import java.lang.*?> +<?import java.util.*?> +<?import javafx.collections.*?> +<?import javafx.geometry.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.layout.*?> +<?import javafx.scene.paint.*?> + +<fx:root type="javafx.scene.layout.AnchorPane" id="AnchorPane" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minHeight="-Infinity" minWidth="0.0" prefHeight="-1.0" prefWidth="-1.0" style="" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2"> + <children> + <HBox alignment="CENTER_LEFT" fillHeight="true" prefHeight="-1.0" prefWidth="-1.0" spacing="5.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"> + <children> + <CheckBox fx:id="selectedBox" mnemonicParsing="false" prefHeight="-1.0" prefWidth="-1.0" selected="true" text="" HBox.hgrow="NEVER" /> + <ChoiceBox fx:id="comparisonBox" minHeight="-1.0" minWidth="0.0" prefHeight="-1.0" prefWidth="0.0" style=" -fx-background-color: linear-gradient(to bottom, derive(-fx-base,-30%), derive(-fx-base,-60%)), linear-gradient(to bottom, derive(-fx-base,65%) 2%, derive(-fx-base,-20%) 95%);" visible="false" HBox.hgrow="NEVER"> + <items> + <FXCollections fx:factory="observableArrayList"> + <String fx:value="Item 1" /> + <String fx:value="Item 2" /> + <String fx:value="Item 3" /> + </FXCollections> + </items> + </ChoiceBox> + <Label fx:id="filterLabel" minWidth="50.0" text="Label" wrapText="true" HBox.hgrow="NEVER" /> + </children> + <padding> + <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> + </padding> + </HBox> + </children> +</fx:root> diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/FilterRow.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/FilterRow.java new file mode 100644 index 0000000000000000000000000000000000000000..3eb13cd0ce83de90d58f364f5e97f650d8549edf --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/FilterRow.java @@ -0,0 +1,17 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.sleuthkit.autopsy.imageanalyzer.filtering.filters; + +import org.sleuthkit.autopsy.imageanalyzer.filtering.filters.AbstractFilter; +import javafx.scene.layout.AnchorPane; + +/** + * + * @author jmillman + */ +public abstract class FilterRow<T extends AbstractFilter> extends AnchorPane { + + public abstract T getFilter(); +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/FilterSet.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/FilterSet.java new file mode 100644 index 0000000000000000000000000000000000000000..88c19e4056665f888cdd6f33d627060f225fde11 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/FilterSet.java @@ -0,0 +1,17 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.sleuthkit.autopsy.imageanalyzer.filtering.filters; + +/** + * + * @author jonathan + */ +public class FilterSet extends IntersectionFilter<AttributeFilter> { + + @Override + public String getDisplayName() { + return "Filters"; + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/IntersectionFilter.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/IntersectionFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..8e4b457885d43b0a36a9d72e9e6fcd486fc4f786 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/IntersectionFilter.java @@ -0,0 +1,38 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.sleuthkit.autopsy.imageanalyzer.filtering.filters; + +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +/** + * + * Intersection(And) filter + */ +public abstract class IntersectionFilter<T extends AbstractFilter> extends CompoundFilter<T> { + + public IntersectionFilter(ObservableList< T> subFilters) { + super(subFilters); + } + + public IntersectionFilter() { + super(FXCollections.<T>observableArrayList()); + } + + @Override + public Boolean accept(DrawableFile df) { + if (isActive()) { + for (T f : subFilters) { + if (f.isActive() && f.accept(df) == false) { + return false; + } + } + return true; + } else { + return true; + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/NameFilter.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/NameFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..08b0f4305efe05d485c15517e7614c6ea375429b --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/NameFilter.java @@ -0,0 +1,70 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.sleuthkit.autopsy.imageanalyzer.filtering.filters; + +import org.sleuthkit.autopsy.imageanalyzer.FXMLConstructor; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute; +import java.net.URL; +import java.util.ResourceBundle; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.fxml.FXML; +import javafx.scene.control.TextField; + +/** + * + */ +public class NameFilter extends AtomicFilter<String, String> { + + public NameFilter() { + super(DrawableAttribute.NAME, SUBSTRING, ""); + } + + @Override + public FilterRow getUI() { + return new NameRow(this); + } + + private void setValue(String text) { + filterValue.set(text); + } + + private static class NameRow extends FilterRow<NameFilter> { + + @FXML + private ResourceBundle resources; + @FXML + private URL location; + @FXML + private TextField textField; + private final NameFilter filter; + + @FXML + void initialize() { + assert textField != null : "fx:id=\"textField\" was not injected: check your FXML file 'NameFilterRow.fxml'."; + + textField.textProperty().addListener(new InvalidationListener() { + @Override + public void invalidated(Observable observable) { + filter.setValue(textField.getText()); + } + }); + +// //hack to make listener hear that this filter has changed +// filter.active.set(!filter.isActive()); +// filter.active.set(!filter.isActive()); + } + + private NameRow(NameFilter filter) { + this.filter = filter; + FXMLConstructor.construct(this, "NameFilterRow.fxml"); + } + + @Override + public NameFilter getFilter() { + return filter; + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/NameFilterRow.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/NameFilterRow.fxml new file mode 100644 index 0000000000000000000000000000000000000000..a51b6c5081591960ca1390a868206e55033dcac9 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/NameFilterRow.fxml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import java.lang.*?> +<?import java.util.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.layout.*?> +<?import javafx.scene.paint.*?> + +<fx:root type="javafx.scene.layout.AnchorPane" id="AnchorPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="200.0" prefHeight="-1.0" prefWidth="-1.0" style="" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2"> + <children> + <TextField fx:id="textField" disable="false" editable="true" focusTraversable="false" maxHeight="-Infinity" maxWidth="-Infinity" minWidth="-1.0" prefColumnCount="12" prefWidth="-1.0" promptText="enter search term" style="" text="" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" /> + </children> +</fx:root> diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/SqlFilter.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/SqlFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..75d530a5e587e37c4c131f9bdf8eae3a9ee6fd3e --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/SqlFilter.java @@ -0,0 +1,18 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.sleuthkit.autopsy.imageanalyzer.filtering.filters; + +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile; + +/** + * + * + */ +public interface SqlFilter { + + public Boolean accept(DrawableFile df); + + public String getFilterQueryString(); +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/UnionFilter.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/UnionFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..898aff3947136ef5d4c109668b88d0e8dddf1a1c --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/filtering/filters/UnionFilter.java @@ -0,0 +1,39 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.sleuthkit.autopsy.imageanalyzer.filtering.filters; + +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +/** + * + * Union(or) filter + */ +abstract public class UnionFilter<T extends AbstractFilter> extends CompoundFilter<T> { + + public UnionFilter(ObservableList<T> subFilters) { + super(subFilters); + } + + public UnionFilter() { + super(FXCollections.<T>observableArrayList()); + } + + @Override + public Boolean accept(DrawableFile df) { + if (isActive()) { + for (T f : subFilters) { + if (f.isActive() && f.accept(df) == true) { + return true; + } + } + return false; + } else { + return true; + } + + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupKey.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupKey.java new file mode 100644 index 0000000000000000000000000000000000000000..47d367cc985e03829263809536dc4b9e7835126b --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupKey.java @@ -0,0 +1,98 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.grouping; + +import java.util.Map; +import java.util.Objects; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute; +import org.sleuthkit.datamodel.TagName; + +/** + * key identifying information of a {@link Grouping}. Used to look up groups in + * {@link Map}s and from the db. + */ +public class GroupKey<T> implements Comparable<GroupKey<Comparable<T>>> { + + private final Comparable<T> val; + + public Comparable<T> getValue() { + return val; + } + + public DrawableAttribute<T> getAttribute() { + return attr; + } + + private final DrawableAttribute<T> attr; + + public GroupKey(DrawableAttribute<T> attr, Comparable<T> val) { + this.attr = attr; + this.val = val; + } + + @Override + public int hashCode() { + int hash = 5; + hash = 29 * hash + Objects.hashCode(this.val); + hash = 29 * hash + Objects.hashCode(this.attr); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final GroupKey<T> other = (GroupKey<T>) obj; + if (!Objects.equals(this.val, other.val)) { + return false; + } + if (this.attr != other.attr) { + return false; + } + return true; + } + + @Override + public int compareTo(GroupKey<Comparable<T>> o) { + if (val instanceof Comparable) { + return ((Comparable) val).compareTo(o.val); + } else { + return Integer.compare(val.hashCode(), o.val.hashCode()); + } + + } + + public String getValueDisplayName() { + if (attr == DrawableAttribute.TAGS) { + return ((TagName) getValue()).getDisplayName(); + } else { + + return getValue().toString(); + } + } + + @Override + public String toString() { + return "GroupKey: " + getAttribute() + " = " + getValue(); + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupManager.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupManager.java new file mode 100644 index 0000000000000000000000000000000000000000..28819748732c5142e500fff0ebffa044b48a7051 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupManager.java @@ -0,0 +1,635 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.grouping; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.logging.Level; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; +import javax.swing.SortOrder; +import org.apache.commons.lang3.StringUtils; +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.coreutils.Logger; +import org.sleuthkit.autopsy.imageanalyzer.EurekaController; +import org.sleuthkit.autopsy.imageanalyzer.EurekaModule; +import org.sleuthkit.autopsy.imageanalyzer.LoggedTask; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.Category; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableDB; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.ContentTag; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TagName; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Provides an abstraction layer on top of {@link DrawableDB} ( and to some + * extent {@link SleuthkitCase} ) to facilitate creation, retrieval, updating, + * and sorting of {@link Grouping}s. + */ +public class GroupManager { + + private static final Logger LOGGER = Logger.getLogger(GroupManager.class.getName()); + + private DrawableDB db; + + private final EurekaController controller; + + /** map from {@link GroupKey}s to {@link Grouping}s. All groups (even not + * fully analyzed or not visible groups could be in this map */ + private final Map<GroupKey, Grouping> groupMap = new HashMap<>(); + + /** list of all analyzed groups */ + private final ObservableList<Grouping> analyzedGroups = FXCollections.observableArrayList(); + + /** list of unseen groups */ + private final ObservableList<Grouping> unSeenGroups = FXCollections.observableArrayList(); + + /** sorted list of unseen groups */ + private final SortedList<Grouping> sortedUnSeenGroups = unSeenGroups.sorted(); + + private ReGroupTask groupByTask; + + /* --- current grouping/sorting attributes --- */ + private volatile GroupSortBy sortBy = GroupSortBy.NONE; + + private volatile DrawableAttribute groupBy = DrawableAttribute.PATH; + + private volatile SortOrder sortOrder = SortOrder.ASCENDING; + + public void setDB(DrawableDB db) { + this.db = db; + regroup(groupBy, sortBy, sortOrder, Boolean.TRUE); + } + + public ObservableList<Grouping> getAnalyzedGroups() { + return analyzedGroups; + } + + public SortedList<Grouping> getUnSeenGroups() { + return sortedUnSeenGroups; + } + + /** + * construct a group manager hooked up to the given db and controller + * + * @param db + * @param controller + */ + public GroupManager(EurekaController controller) { + this.controller = controller; + } + + /** + * using the current groupBy set for this manager, find groupkeys for all + * the groups the given file is a part of + * + * @param file + * + * @returna a set of {@link GroupKey}s representing the group(s) the given + * file is a part of + */ + public Set<GroupKey> getGroupKeysForFile(DrawableFile file) { + Set<GroupKey> resultSet = new HashSet(); + final Object valueOfAttribute = file.getValueOfAttribute(groupBy); + List<? extends Comparable> vals; + if (valueOfAttribute instanceof List<?>) { + vals = (List<? extends Comparable>) valueOfAttribute; + } else { + vals = Collections.singletonList((Comparable) valueOfAttribute); + } + for (Comparable val : vals) { + final GroupKey groupKey = new GroupKey(groupBy, val); + resultSet.add(groupKey); + } + return resultSet; + } + + /** + * using the current groupBy set for this manager, find groupkeys for all + * the groups the given file is a part of + * + * @param file + * + * @returna a set of {@link GroupKey}s representing the group(s) the given + * file is a part of + */ + public Set<GroupKey> getGroupKeysForFileID(Long fileID) { + Set<GroupKey> resultSet = new HashSet(); + try { + DrawableFile file = db.getFileFromID(fileID); + final Object valueOfAttribute = file.getValueOfAttribute(groupBy); + + List<? extends Comparable> vals; + if (valueOfAttribute instanceof List<?>) { + vals = (List<? extends Comparable>) valueOfAttribute; + } else { + vals = Collections.singletonList((Comparable) valueOfAttribute); + } + + for (Comparable val : vals) { + final GroupKey groupKey = new GroupKey(groupBy, val); + resultSet.add(groupKey); + } + } catch (TskCoreException ex) { + Logger.getLogger(GroupManager.class.getName()).log(Level.SEVERE, "failed to load file with id: " + fileID + " from database", ex); + } + return resultSet; + } + + /** + * @param groupKey + * + * @return return the Grouping (if it exists) for the given GroupKey, or + * null if no group exists for that key. + */ + public Grouping getGroupForKey(GroupKey groupKey) { + synchronized (groupMap) { + return groupMap.get(groupKey); + } + } + + synchronized public void clear() { + + if (groupByTask != null) { + groupByTask.cancel(true); + } + sortBy = GroupSortBy.GROUP_BY_VALUE; + groupBy = DrawableAttribute.PATH; + sortOrder = SortOrder.ASCENDING; + Platform.runLater(() -> { + unSeenGroups.clear(); + analyzedGroups.clear(); + }); + synchronized (groupMap) { + groupMap.clear(); + } + db = null; + } + + public boolean isRegrouping() { + if (groupByTask == null) { + return false; + } + + switch (groupByTask.getState()) { + case READY: + case RUNNING: + case SCHEDULED: + return true; + case CANCELLED: + case FAILED: + + case SUCCEEDED: + default: + return false; + } + } + + /** + * make and return a group with the given key and files. If a group already + * existed for that key, it will be replaced. + * + * @param groupKey + * @param files + * + * @return + * + * TODO: check if a group already exists for that key and ... (do what?add + * files to it?) -jm + */ + public Grouping makeGroup(GroupKey groupKey, List<Long> files) { + List<Long> newFiles = files == null ? new ArrayList<>() : files; + + Grouping g = new Grouping(groupKey, newFiles); + synchronized (groupMap) { + groupMap.put(groupKey, g); + } + return g; + } + + public void markGroupSeen(Grouping group) { + unSeenGroups.remove(group); + db.markGroupSeen(group.groupKey); + } + + /** + * remove the given file from the group with the given key. If the group + * doesn't exist or doesn't already contain this file, this method is a + * no-op + * + * @param groupKey the value of groupKey + * @param fileID the value of file + */ + public synchronized void removeFromGroup(GroupKey groupKey, final Long fileID) { + //get grouping this file would be in + final Grouping group = getGroupForKey(groupKey); + if (group != null) { + group.removeFile(fileID); + } + } + + public synchronized void populateAnalyzedGroup(final GroupKey groupKey, List<Long> filesInGroup) { + populateAnalyzedGroup(groupKey, filesInGroup, null); + } + + /** + * create a group with the given GroupKey and file ids and add it to the + * analyzed group list. + * + * @param groupKey + * @param filesInGroup + */ + private synchronized void populateAnalyzedGroup(final GroupKey groupKey, List<Long> filesInGroup, ReGroupTask task) { + + /* if this is not part of a regroup task or it is but the task is not + * cancelled... + * + * this allows us to stop if a regroup task has been cancelled (e.g. the + * user picked a different group by attribute, while the current task + * was still running) */ + if (task == null || (task.isCancelled() == false)) { + Grouping g = makeGroup(groupKey, filesInGroup); + + final boolean groupSeen = db.isGroupSeen(groupKey); + Platform.runLater(() -> { + analyzedGroups.add(g); + + if (groupSeen == false) { + unSeenGroups.add(g); + } + }); + } + } + + /** + * check if the group for the given groupkey is analyzed + * + * @param groupKey + * + * @return null if this group is not analyzed or a list of file ids in this + * group if they are all analyzed + */ + public List<Long> checkAnalyzed(final GroupKey groupKey) { + try { + /* for attributes other than path we can't be sure a group is fully + * analyzed because we don't know all the files that will be a part + * of that group */ + if ((groupKey.getAttribute() != DrawableAttribute.PATH) || db.isGroupAnalyzed(groupKey)) { + return getFileIDsInGroup(groupKey); + } else { + return null; + } + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "failed to get files for group: " + groupKey.getAttribute().attrName.name() + " = " + groupKey.getValue(), ex); + return null; + } + } + + /** + * the implementation of this should be moved to DrawableDB + * + * @param hashDbName + * + * @return + * + * @deprecated + */ + @Deprecated + private List<Long> getFileIDsWithHashSetName(String hashDbName) { + List<Long> files = new ArrayList<>(); + try { + + final SleuthkitCase sleuthkitCase = EurekaController.getDefault().getSleuthKitCase(); + String query = "SELECT obj_id FROM blackboard_attributes,blackboard_artifacts WHERE " + + "attribute_type_id=" + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID() + + " AND blackboard_attributes.artifact_id=blackboard_artifacts.artifact_id" + + " AND blackboard_attributes.value_text='" + hashDbName + "'" + + " AND blackboard_artifacts.artifact_type_id=" + BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID(); + + ResultSet rs = null; + try { + rs = sleuthkitCase.runQuery(query); + while (rs.next()) { + long id = rs.getLong("obj_id"); + try { + if (EurekaModule.isSupportedAndNotKnown(Case.getCurrentCase().getSleuthkitCase().getAbstractFileById(id))) { + files.add(id); + } + } catch (TskCoreException ex) { + Exceptions.printStackTrace(ex); + } + } + } catch (SQLException ex) { + Exceptions.printStackTrace(ex); + } finally { + if (rs != null) { + try { + Case.getCurrentCase().getSleuthkitCase().closeRunQuery(rs); + } catch (SQLException ex) { + LOGGER.log(Level.WARNING, "Error closing result set after getting hashset hits", ex); + } + } + } + + return files; + } catch (IllegalStateException ex) { + LOGGER.log(Level.SEVERE, "Can't get the current case; there is no case open!", ex); + return files; + } + } + + /** + * find the distinct values for the given column (DrawableAttribute) in the + * order given by sortBy and sortOrder. + * + * These values represent the groups of files. + * + * @param regroup + * @param sortBy + * @param sortOrder + * + * @return + */ + public <A extends Comparable> List<A> findValuesForAttribute(DrawableAttribute<A> groupBy, GroupSortBy sortBy, SortOrder sortOrder) { + try { + List<A> values; + switch (groupBy.attrName) { + //these cases get special treatment + case CATEGORY: + values = (List<A>) Category.valuesList(); + break; + case TAGS: + values = (List<A>) Case.getCurrentCase().getServices().getTagsManager().getTagNamesInUse(); + break; + case ANALYZED: + values = (List<A>) Arrays.asList(false, true); + break; + case HASHSET: + TreeSet<A> names = new TreeSet<>((Set<A>) db.getHashSetNames()); + values = new ArrayList<>(names); + break; + default: + //otherwise do straight db query + return db.findValuesForAttribute(groupBy, sortBy, sortOrder); + } + + //sort in memory + Collections.sort(values, sortBy.getValueComparator(groupBy, sortOrder)); + + return values; + } catch (TskCoreException ex) { + Exceptions.printStackTrace(ex); + return new ArrayList<>(); + } + } + + /** + * find the distinct values of the regroup attribute in the order given by + * sortBy with a ascending order + * + * @param regroup + * @param sortBy + * + * @return + */ + public <A extends Comparable> List<A> findValuesForAttribute(DrawableAttribute<A> groupBy, GroupSortBy sortBy) { + return findValuesForAttribute(groupBy, sortBy, SortOrder.ASCENDING); + } + + public List<Long> getFileIDsInGroup(GroupKey groupKey) throws TskCoreException { + switch (groupKey.getAttribute().attrName) { + //these cases get special treatment + case CATEGORY: + return getFileIDsWithCategory((Category) groupKey.getValue()); + case TAGS: + return getFileIDsWithTag((TagName) groupKey.getValue()); + case HASHSET: //comment out this case to use db functionality for hashsets + return getFileIDsWithHashSetName((String) groupKey.getValue()); + default: + //straight db query + return db.getFileIDsInGroup(groupKey); + } + } + + // @@@ This was kind of slow in the profiler. Maybe we should cache it. + public List<Long> getFileIDsWithCategory(Category category) throws TskCoreException { + + try { + if (category == Category.ZERO) { + + List<Long> files = new ArrayList<>(); + TagName[] tns = {Category.FOUR.getTagName(), Category.THREE.getTagName(), Category.TWO.getTagName(), Category.ONE.getTagName(), Category.FIVE.getTagName()}; + for (TagName tn : tns) { + List<ContentTag> contentTags = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByTagName(tn); + for (ContentTag ct : contentTags) { + if (ct.getContent() instanceof AbstractFile && EurekaModule.isSupportedAndNotKnown((AbstractFile) ct.getContent())) { + files.add(ct.getContent().getId()); + } + } + } + + return db.findAllFileIdsWhere("obj_id NOT IN (" + StringUtils.join(files, ',') + ")"); + } else { + + List<Long> files = new ArrayList<>(); + List<ContentTag> contentTags = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByTagName(category.getTagName()); + for (ContentTag ct : contentTags) { + if (ct.getContent() instanceof AbstractFile && EurekaModule.isSupportedAndNotKnown((AbstractFile) ct.getContent())) { + files.add(ct.getContent().getId()); + } + } + + return files; + } + } catch (TskCoreException ex) { + LOGGER.log(Level.WARNING, "TSK error getting files in Category:" + category.getDisplayName(), ex); + throw ex; + } + } + + public List<Long> getFileIDsWithTag(TagName tagName) throws TskCoreException { + try { + List<Long> files = new ArrayList<>(); + List<ContentTag> contentTags = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByTagName(tagName); + for (ContentTag ct : contentTags) { + if (ct.getContent() instanceof AbstractFile && EurekaModule.isSupportedAndNotKnown((AbstractFile) ct.getContent())) { + files.add(ct.getContent().getId()); + } + } + + return files; + } catch (TskCoreException ex) { + LOGGER.log(Level.WARNING, "TSK error getting files with Tag:" + tagName.getDisplayName(), ex); + throw ex; + } + } + + public GroupSortBy getSortBy() { + return sortBy; + } + + public void setSortBy(GroupSortBy sortBy) { + this.sortBy = sortBy; + } + + public DrawableAttribute getGroupBy() { + return groupBy; + } + + public void setGroupBy(DrawableAttribute groupBy) { + this.groupBy = groupBy; + } + + public SortOrder getSortOrder() { + return sortOrder; + } + + public void setSortOrder(SortOrder sortOrder) { + this.sortOrder = sortOrder; + } + + /** + * regroup all files in the database using given {@link DrawableAttribute} + * see + * {@link ReGroupTask} for more details. + * + * @param groupBy + * @param sortBy + * @param sortOrder + * @param force true to force a full db query regroup + */ + public void regroup(final DrawableAttribute groupBy, final GroupSortBy sortBy, final SortOrder sortOrder, Boolean force) { + //only re-query the db if the group by attribute changed or it is forced + if (groupBy != getGroupBy() || force == true) { + setGroupBy(groupBy); + setSortBy(sortBy); + setSortOrder(sortOrder); + if (groupByTask != null) { + groupByTask.cancel(true); + } + sortedUnSeenGroups.setComparator(sortBy.getGrpComparator(groupBy, sortOrder)); + groupByTask = new ReGroupTask(groupBy, sortBy, sortOrder); + controller.submitBGTask(groupByTask); + } else { + // just resort the list of groups + setSortBy(sortBy); + setSortOrder(sortOrder); + sortedUnSeenGroups.setComparator(sortBy.getGrpComparator(groupBy, sortOrder)); + } + } + + /** + * Task to query database for files in sorted groups and build + * {@link Groupings} for them + */ + private class ReGroupTask extends LoggedTask<Void> { + + private ProgressHandle groupProgress; + + private final DrawableAttribute groupBy; + + private final GroupSortBy sortBy; + + private final SortOrder sortOrder; + + public ReGroupTask(DrawableAttribute groupBy, GroupSortBy sortBy, SortOrder sortOrder) { + super("regrouping files by " + groupBy.attrName.name() + " sorted by " + sortBy.name() + " in " + sortOrder.name() + " order", true); + + this.groupBy = groupBy; + this.sortBy = sortBy; + this.sortOrder = sortOrder; + } + + @Override + public boolean isCancelled() { + return super.isCancelled() || groupBy != getGroupBy() || sortBy != getSortBy() || sortOrder != getSortOrder(); + } + + @Override + protected Void call() throws Exception { + + if (isCancelled()) { + return null; + } + + groupProgress = ProgressHandleFactory.createHandle(getTaskName(), this); + Platform.runLater(() -> { + analyzedGroups.clear(); + unSeenGroups.clear(); + }); + groupMap.clear(); + + //get a list of group key vals + final List<Comparable> vals = findValuesForAttribute(groupBy, sortBy, sortOrder); + + groupProgress.start(vals.size()); + + int p = 0; + //for each key value + for (final Comparable val : vals) { + if (isCancelled()) { + return null;//abort + } + p++; + updateMessage("regrouping files by " + groupBy.attrName.name() + " : " + val); + updateProgress(p, vals.size()); + groupProgress.progress("regrouping files by " + groupBy.attrName.name() + " : " + val, p); + //check if this group is analyzed + final GroupKey groupKey = new GroupKey(groupBy, val); + + List<Long> checkAnalyzed = checkAnalyzed(groupKey); + if (checkAnalyzed != null) { // != null => the group is analyzed, so add it to the ui + populateAnalyzedGroup(groupKey, checkAnalyzed, ReGroupTask.this); + } + } + updateProgress(1, 1); + return null; + } + + @Override + protected void done() { + super.done(); + if (groupProgress != null) { + groupProgress.finish(); + groupProgress = null; + } + +// controller.checkForGroups(); + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupSortBy.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupSortBy.java new file mode 100644 index 0000000000000000000000000000000000000000..ca3a3ef792c3bb8ac5641658bb523762b58732f9 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupSortBy.java @@ -0,0 +1,202 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-14 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.grouping; + +import java.util.Arrays; +import java.util.Comparator; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.image.Image; +import javax.swing.SortOrder; +import static javax.swing.SortOrder.ASCENDING; +import static javax.swing.SortOrder.DESCENDING; +import org.apache.commons.lang3.StringUtils; +import org.sleuthkit.autopsy.imageanalyzer.EurekaController; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute; +import static org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute.AttributeName.TAGS; +import org.sleuthkit.datamodel.TagName; + +/** enum of possible properties to sort groups by. This is the model for the + * drop down in {@link EurekaToolbar} as well as each enum value having the + * stategy ({@link Comparator}) for sorting the groups */ +public enum GroupSortBy implements ComparatorProvider { + + /** sort the groups by the number of files in each + * sort the groups by the number of files in each */ + FILE_COUNT("Group Size", true, "folder-open-image.png") { + @Override + public Comparator<Grouping> getGrpComparator(DrawableAttribute attr, final SortOrder sortOrder) { + Comparator<Grouping> comparingInt = Comparator.comparingInt(Grouping::getSize); + return sortOrder == ASCENDING ? comparingInt : comparingInt.reversed(); + } + + @Override + public <A extends Comparable> Comparator<A> getValueComparator(final DrawableAttribute<A> attr, final SortOrder sortOrder) { + return (A v1, A v2) -> { + Grouping g1 = EurekaController.getDefault().getGroupManager().getGroupForKey(new GroupKey(attr, v1)); + Grouping g2 = EurekaController.getDefault().getGroupManager().getGroupForKey(new GroupKey(attr, v2)); + return getGrpComparator(attr, sortOrder).compare(g1, g2); + }; + } + }, + /** sort + * the groups by the natural order of the grouping value ( eg group them by + * path alphabetically ) */ + GROUP_BY_VALUE("Group Name", true, "folder-rename.png") { + @Override + public Comparator<Grouping> getGrpComparator(final DrawableAttribute attr, final SortOrder sortOrder) { + return (Grouping o1, Grouping o2) -> { + + final Comparable c1 = o1.groupKey.getValue(); + final Comparable c2 = o2.groupKey.getValue(); + final boolean isTags = attr.attrName == TAGS; + + switch (sortOrder) { + case ASCENDING: + return c1.compareTo(c2); + case DESCENDING: + return c1.compareTo(c2) * -1; + default: //unsorted + return 0; + } + }; + } + + @Override + public <A extends Comparable> Comparator<A> getValueComparator(final DrawableAttribute<A> attr, final SortOrder sortOrder) { + return (A o1, A o2) -> { + Comparable c1; + Comparable c2; + switch (attr.attrName) { + case TAGS: + c1 = ((TagName) o1).getDisplayName(); + c2 = ((TagName) o2).getDisplayName(); + break; + default: + c1 = (Comparable) o1; + c2 = (Comparable) o2; + } + + switch (sortOrder) { + case ASCENDING: + return c1.compareTo(c2); + case DESCENDING: + return c1.compareTo(c2) * -1; + default: //unsorted + return 0; + } + }; + } + }, + /** don't sort the groups just use what ever + * order they come in (ingest + * order) */ + /** don't sort the groups just use what ever order they come + * in (ingest + * order) */ + NONE("None", false, "prohibition.png") { + @Override + public Comparator<Grouping> getGrpComparator(DrawableAttribute attr, SortOrder sortOrder) { + return new NoOpComparator<>(); + } + + @Override + public <A extends Comparable> Comparator<A> getValueComparator(DrawableAttribute<A> attr, final SortOrder sortOrder) { + return new NoOpComparator<>(); + } + }, + /** sort + * the groups by some priority metric to be determined and implemented */ + PRIORITY("Priority", false, "hashset_hits.png") { + @Override + public Comparator<Grouping> getGrpComparator(DrawableAttribute attr, SortOrder sortOrder) { + return Comparator.nullsLast(Comparator.comparingDouble(Grouping::getHashHitDensity).thenComparing(Grouping::getSize).reversed()); + } + + @Override + public <A extends Comparable> Comparator<A> getValueComparator(DrawableAttribute<A> attr, SortOrder sortOrder) { + return (A v1, A v2) -> { + Grouping g1 = EurekaController.getDefault().getGroupManager().getGroupForKey(new GroupKey(attr, v1)); + Grouping g2 = EurekaController.getDefault().getGroupManager().getGroupForKey(new GroupKey(attr, v2)); + + return getGrpComparator(attr, sortOrder).compare(g1, g2); + }; + } + }; + + /** + * get a list of the values of this enum + * + * @return + */ + public static ObservableList<GroupSortBy> getValues() { + return FXCollections.observableArrayList(Arrays.asList(values())); + + } + + final private String displayName; + + private Image icon; + + private final String imageName; + + private final Boolean sortOrderEnabled; + + private GroupSortBy(String displayName, Boolean sortOrderEnabled, String imagePath) { + this.displayName = displayName; + this.sortOrderEnabled = sortOrderEnabled; + this.imageName = imagePath; + } + + public String getDisplayName() { + return displayName; + } + + public Image getIcon() { + if (icon == null) { + if (StringUtils.isBlank(imageName) == false) { + this.icon = new Image("org/sleuthkit/autopsy/imageanalyzer/images/" + imageName, true); + } + } + return icon; + } + + public Boolean isSortOrderEnabled() { + return sortOrderEnabled; + } + + private static class NoOpComparator<A> implements Comparator<A> { + + @Override + public int compare(A o1, A o2) { + return 0; + } + } +} + +/** * implementers of this interface must provide a method to compare + * ({@link Comparable}) values and Groupings based on an + * {@link DrawableAttribute} and a {@link SortOrder} + */ +interface ComparatorProvider { + + <A extends Comparable> Comparator<A> getValueComparator(DrawableAttribute<A> attr, SortOrder sortOrder); + + <A> Comparator<Grouping> getGrpComparator(DrawableAttribute<A> attr, SortOrder sortOrder); +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupViewMode.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupViewMode.java new file mode 100644 index 0000000000000000000000000000000000000000..01e985679b477673c8f5688cc496956ac0e2865b --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupViewMode.java @@ -0,0 +1,7 @@ +package org.sleuthkit.autopsy.imageanalyzer.grouping; + +public enum GroupViewMode { + + TILE, SLIDE_SHOW + +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupViewState.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupViewState.java new file mode 100644 index 0000000000000000000000000000000000000000..15666e795072cbab7461193d03b17d7650b70958 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/GroupViewState.java @@ -0,0 +1,91 @@ + +/* Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.grouping; + +import java.util.Objects; +import java.util.Optional; + +/** + * + */ +public class GroupViewState { + + private final Grouping group; + + private final GroupViewMode mode; + + private final Optional<Long> slideShowfileID; + + public Grouping getGroup() { + return group; + } + + public GroupViewMode getMode() { + return mode; + } + + public Optional<Long> getSlideShowfileID() { + return slideShowfileID; + } + + private GroupViewState(Grouping g, GroupViewMode mode, Long slideShowfileID) { + this.group = g; + this.mode = mode; + this.slideShowfileID = Optional.ofNullable(slideShowfileID); + } + + public static GroupViewState tile(Grouping g) { + return new GroupViewState(g, GroupViewMode.TILE, null); + } + + public static GroupViewState slideShow(Grouping g, Long fileID) { + return new GroupViewState(g, GroupViewMode.SLIDE_SHOW, fileID); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 17 * hash + Objects.hashCode(this.group); + hash = 17 * hash + Objects.hashCode(this.mode); + hash = 17 * hash + Objects.hashCode(this.slideShowfileID); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final GroupViewState other = (GroupViewState) obj; + if (!Objects.equals(this.group, other.group)) { + return false; + } + if (this.mode != other.mode) { + return false; + } + if (!Objects.equals(this.slideShowfileID, other.slideShowfileID)) { + return false; + } + return true; + } + +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/Grouping.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/Grouping.java new file mode 100644 index 0000000000000000000000000000000000000000..d65e73c9de376e5dd5320242619d343d3eedce8c --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/grouping/Grouping.java @@ -0,0 +1,129 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.grouping; + +import org.sleuthkit.autopsy.imageanalyzer.EurekaController; +import java.util.List; +import java.util.Objects; +import java.util.logging.Level; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Represents a set of files in a group. The UI listens to changes to the group + * and updates itself accordingly. + * + * This class is named Grouping and not Group to avoid confusion with + * {@link javafx.scene.Group} and others. + */ +public class Grouping { + + private static final Logger LOGGER = Logger.getLogger(Grouping.class.getName()); + + public static final String UNKNOWN = "unknown"; + + final private ObservableList<Long> fileIDs = FXCollections.observableArrayList(); + + //cache the number of files in this groups with hashset hits + private int filesWithHashSetHitsCount = -1; + + synchronized public ObservableList<Long> fileIds() { + return fileIDs; + } + + final public GroupKey groupKey; + + public Grouping(GroupKey groupKey, List<Long> filesInGroup) { + this.groupKey = groupKey; + fileIDs.setAll(filesInGroup); + } + + synchronized public Integer getSize() { + return fileIDs.size(); + } + + public double getHashHitDensity() { + return getFilesWithHashSetHitsCount() / getSize().doubleValue(); + } + + synchronized public Integer getFilesWithHashSetHitsCount() { + + if (filesWithHashSetHitsCount < 0) { + filesWithHashSetHitsCount = 0; + for (Long fileID : fileIds()) { + try { + long artcount = EurekaController.getDefault().getSleuthKitCase().getBlackboardArtifactsCount(BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT, fileID); + if (artcount > 0) { + filesWithHashSetHitsCount++; + } + } catch (IllegalStateException | TskCoreException ex) { + LOGGER.log(Level.WARNING, "could not access case during getFilesWithHashSetHitsCount()", ex); + break; + } + } + } + return filesWithHashSetHitsCount; + } + + @Override + public String toString() { + return "Grouping{ keyProp=" + groupKey + '}'; + } + + @Override + public int hashCode() { + int hash = 3; + hash = 53 * hash + Objects.hashCode(this.groupKey); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Grouping other = (Grouping) obj; + if (!Objects.equals(this.groupKey, other.groupKey)) { + return false; + } + return true; + } + + synchronized public void addFile(Long f) { + Platform.runLater(() -> { + if (fileIDs.contains(f) == false) { + fileIDs.add(f); + } + }); + + } + + synchronized public void removeFile(Long f) { + Platform.runLater(() -> { + fileIDs.removeAll(f); + }); + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/AttributeListCell.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/AttributeListCell.java new file mode 100644 index 0000000000000000000000000000000000000000..edc9f1b03f93522b2ec94112e3fe87cb38438fd5 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/AttributeListCell.java @@ -0,0 +1,40 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.gui; + +import javafx.scene.control.ListCell; +import javafx.scene.image.ImageView; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute; + +/** + */ +class AttributeListCell extends ListCell<DrawableAttribute<?>> { + + @Override + protected void updateItem(DrawableAttribute<?> item, boolean empty) { + super.updateItem(item, empty); //To change body of generated methods, choose Tools | Templates. + if (item != null) { + setText(item.getDisplayName()); + setGraphic(new ImageView(item.getIcon())); + } else { + setGraphic(null); + setText(null); + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/DrawableTile.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/DrawableTile.fxml new file mode 100644 index 0000000000000000000000000000000000000000..8bfa03862af828ea2e182cad81920da44efa6d74 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/DrawableTile.fxml @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import java.lang.*?> +<?import javafx.geometry.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.image.*?> +<?import javafx.scene.layout.*?> + +<fx:root maxHeight="-1.0" maxWidth="-1.0" minHeight="-Infinity" minWidth="-Infinity" opacity="1.0" prefHeight="-1.0" prefWidth="-1.0" style="-fx-background-color: linear-gradient(to bottom, derive(-fx-base,-30%), derive(-fx-base,-60%)), linear-gradient(to bottom, derive(-fx-base,65%) 2%, derive(-fx-base,-20%) 95%); -fx-background-radius: 2;" type="javafx.scene.layout.AnchorPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> + <children> + <BorderPane maxHeight="-1.0" maxWidth="-1.0" minHeight="-Infinity" minWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" snapToPixel="true" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"> + <bottom> + <BorderPane maxHeight="-Infinity" maxWidth="-1.0" minHeight="-Infinity" minWidth="-1.0" prefHeight="-1.0" prefWidth="-1.0" BorderPane.alignment="CENTER"> + <center> + <Label id="pathLabel" fx:id="nameLabel" alignment="CENTER" contentDisplay="TEXT_ONLY" maxHeight="16.0" minHeight="16.0" minWidth="-1.0" prefHeight="-1.0" prefWidth="-1.0" text="file name" textAlignment="CENTER" wrapText="true"> + <labelFor> + <ImageView fx:id="imageView" fitHeight="200.0" fitWidth="200.0" opacity="1.0" pickOnBounds="true" preserveRatio="true" style="-fx-border-radius : 5; -fx-border-width : 5; -fx-border-color : blue;" BorderPane.alignment="CENTER" /> + </labelFor> + </Label> + </center> + <left> + <HBox maxHeight="-Infinity" prefHeight="-1.0" prefWidth="-1.0" spacing="2.0" BorderPane.alignment="CENTER_LEFT"> + <children> + <ImageView fx:id="fileTypeImageView" fitHeight="16.0" fitWidth="16.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true" scaleX="1.0" scaleY="1.0"> + <image> + <Image url="@../images/video-file.png" /> + </image> + </ImageView> + <ImageView fx:id="hashHitImageView" fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" style=""> + <image> + <Image url="@../images/hashset_hits.png" /> + </image> + <HBox.margin> + <Insets bottom="1.0" left="1.0" right="1.0" top="1.0" fx:id="x1" /> + </HBox.margin> + </ImageView> + </children> + <padding> + <Insets bottom="2.0" right="2.0" top="2.0" /> + </padding> + </HBox> + </left> + <right> + <ToggleButton fx:id="followUpToggle" minWidth="24.0" mnemonicParsing="false" prefWidth="24.0" selected="false" text=""> + <graphic> + <ImageView id="followUpImageview" fx:id="followUpImageView" fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true"> + <image> + <Image url="@../images/flag_gray.png" /> + </image> + </ImageView> + </graphic> + </ToggleButton> + </right> + </BorderPane> + </bottom> + <center> + <BorderPane fx:id="imageBorder" center="$imageView" maxHeight="-1.0" maxWidth="-1.0" minWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" BorderPane.alignment="CENTER"> +<center><ImageView fx:id="imageView" fitHeight="100.0" fitWidth="100.0" pickOnBounds="true" preserveRatio="true" BorderPane.alignment="CENTER" /> +</center></BorderPane> + </center> + </BorderPane> + </children> + <padding> + <Insets bottom="2.0" left="2.0" right="2.0" top="2.0" /> + </padding> +</fx:root> diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/DrawableTile.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/DrawableTile.java new file mode 100644 index 0000000000000000000000000000000000000000..aab1928ca9e2b747faf359072461984695d021c0 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/DrawableTile.java @@ -0,0 +1,122 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.gui; + +import java.net.URL; +import java.util.Objects; +import java.util.ResourceBundle; +import javafx.fxml.FXML; +import javafx.scene.CacheHint; +import javafx.scene.control.Control; +import javafx.scene.effect.DropShadow; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.paint.Color; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.imageanalyzer.FXMLConstructor; +import org.sleuthkit.autopsy.imageanalyzer.TagUtils; +import org.sleuthkit.autopsy.imageanalyzer.ThreadConfined; +import org.sleuthkit.autopsy.imageanalyzer.ThreadConfined.ThreadType; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.Category; + +/** + * GUI component that represents a single image as a tile with an icon, a label + * a color coded border and possibly other controls. Designed to be in a + * {@link GroupPane}'s TilePane or SlideShow. + * + * + * TODO: refactor this to extend from {@link Control}? -jm + */ +public class DrawableTile extends SingleDrawableViewBase implements Category.CategoryListener, TagUtils.TagListener { + + private static final DropShadow LAST_SELECTED_EFFECT = new DropShadow(10, Color.BLUE); + + private static final Logger LOGGER = Logger.getLogger(DrawableTile.class.getName()); + + /** + * the central ImageView that shows a thumbnail of the represented file + */ + @FXML + @ThreadConfined(type = ThreadType.UI) + private ImageView imageView; + + public ImageView getImageView() { + return imageView; + } + + @FXML + private ResourceBundle resources; + + @FXML + private URL location; + + @Override + protected void disposeContent() { + //no-op + } + + @FXML + @Override + protected void initialize() { + super.initialize(); + assert imageBorder != null : "fx:id=\"imageAnchor\" was not injected: check your FXML file 'DrawableTile.fxml'."; + assert imageView != null : "fx:id=\"imageView\" was not injected: check your FXML file 'DrawableTile.fxml'."; + assert nameLabel != null : "fx:id=\"nameLabel\" was not injected: check your FXML file 'DrawableTile.fxml'."; + + //set up properties and binding + setCache(true); + setCacheHint(CacheHint.SPEED); + nameLabel.prefWidthProperty().bind(imageView.fitWidthProperty()); + + imageView.fitHeightProperty().bind(EurekaToolbar.getDefault().sizeSliderValue()); + imageView.fitWidthProperty().bind(EurekaToolbar.getDefault().sizeSliderValue()); + + globalSelectionModel.lastSelectedProperty().addListener((observable, oldValue, newValue) -> { + setEffect(Objects.equals(newValue, fileID) ? LAST_SELECTED_EFFECT : null); + }); + } + + public DrawableTile(GroupPane gp) { + super(); + FXMLConstructor.construct(this, "DrawableTile.fxml"); + groupPane = gp; + } + + @Override + @ThreadConfined(type = ThreadType.UI) + protected void clearContent() { + imageView.setImage(null); + } + + @Override + protected Runnable getContentUpdateRunnable() { + Image image = file.getIcon(); + + return () -> { + imageView.setImage(image); + }; + } + + @Override + @ThreadConfined(type = ThreadType.UI) + protected String getLabelText() { + return file.getName(); + } + +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/DrawableView.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/DrawableView.java new file mode 100644 index 0000000000000000000000000000000000000000..aec3246430474e05f0eca70bffa45b98c480c873 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/DrawableView.java @@ -0,0 +1,93 @@ +package org.sleuthkit.autopsy.imageanalyzer.gui; + +import java.util.Collection; +import javafx.application.Platform; +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.Region; +import javafx.scene.paint.Color; +import org.sleuthkit.autopsy.imageanalyzer.TagUtils; +import org.sleuthkit.autopsy.imageanalyzer.ThreadConfined; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.Category; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile; + +/** + * TODO: extract common interface out of {@link SingleImageView} and + * {@link MetaDataPane} + */ +public interface DrawableView extends Category.CategoryListener, TagUtils.TagListener { + + //TODO: do this all in css? -jm + static final int CAT_BORDER_WIDTH = 10; + + static final BorderWidths CAT_BORDER_WIDTHS = new BorderWidths(CAT_BORDER_WIDTH); + + static final CornerRadii CAT_CORNER_RADII = new CornerRadii(3); + + static final Border HASH_BORDER = new Border(new BorderStroke(Color.PURPLE, BorderStrokeStyle.DASHED, CAT_CORNER_RADII, CAT_BORDER_WIDTHS)); + + static final Border CAT1_BORDER = new Border(new BorderStroke(Category.ONE.getColor(), BorderStrokeStyle.SOLID, CAT_CORNER_RADII, CAT_BORDER_WIDTHS)); + + static final Border CAT2_BORDER = new Border(new BorderStroke(Category.TWO.getColor(), BorderStrokeStyle.SOLID, CAT_CORNER_RADII, CAT_BORDER_WIDTHS)); + + static final Border CAT3_BORDER = new Border(new BorderStroke(Category.THREE.getColor(), BorderStrokeStyle.SOLID, CAT_CORNER_RADII, CAT_BORDER_WIDTHS)); + + static final Border CAT4_BORDER = new Border(new BorderStroke(Category.FOUR.getColor(), BorderStrokeStyle.SOLID, CAT_CORNER_RADII, CAT_BORDER_WIDTHS)); + + static final Border CAT5_BORDER = new Border(new BorderStroke(Category.FIVE.getColor(), BorderStrokeStyle.SOLID, CAT_CORNER_RADII, CAT_BORDER_WIDTHS)); + + static final Border CAT0_BORDER = new Border(new BorderStroke(Category.ZERO.getColor(), BorderStrokeStyle.SOLID, CAT_CORNER_RADII, CAT_BORDER_WIDTHS)); + + Region getBorderable(); + + DrawableFile getFile(); + + void setFile(final Long fileID); + + Long getFileID(); + + @Override + void handleCategoryChanged(Collection<Long> ids); + + @Override + void handleTagsChanged(Collection<Long> ids); + + default boolean hasHashHit() { + return getFile().getHashHitSetNames().isEmpty() == false; + } + + static Border getCategoryBorder(Category category) { + switch (category) { + case ZERO: + return CAT0_BORDER; + case ONE: + return CAT1_BORDER; + case TWO: + return CAT2_BORDER; + case THREE: + return CAT3_BORDER; + case FOUR: + return CAT4_BORDER; + case FIVE: + default: + return CAT5_BORDER; + } + } + + @ThreadConfined(type = ThreadConfined.ThreadType.ANY) + default Category updateCategoryBorder() { + final Category category = getFile().getCategory(); + final Border border = hasHashHit() && (category == Category.ZERO) + ? HASH_BORDER + : DrawableView.getCategoryBorder(category); + + Platform.runLater(() -> { + getBorderable().setBorder(border); + }); + return category; + } + +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/EurekaPanel.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/EurekaPanel.java new file mode 100644 index 0000000000000000000000000000000000000000..a62b9ed75b79c7cc0b7494db254dd6ad9dd2f3fd --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/EurekaPanel.java @@ -0,0 +1,105 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.gui; + +import org.sleuthkit.autopsy.imageanalyzer.EurekaController; +import org.sleuthkit.autopsy.imageanalyzer.gui.navpanel.NavPanel; +import javafx.application.Platform; +import javafx.embed.swing.JFXPanel; +import javafx.scene.Scene; +import javafx.scene.control.SplitPane; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import org.sleuthkit.autopsy.coreutils.Logger; + +/** JFXPanel derived class that contains Eureka GUI. */ +public class EurekaPanel extends JFXPanel { + + private static final Logger LOGGER = Logger.getLogger(JFXPanel.class.getName()); + + private StackPane centralStack; + + private EurekaController controller; + + private StackPane fullUIStack; + + private SplitPane splitPane; + + private MetaDataPane metaDataTable; + + private GroupPane groupPane; + + private NavPanel navPanel; + + private Scene myScene; + + volatile private boolean sceneInited = false; + + public EurekaPanel() { + + } + + @Override + public void addNotify() { + + super.addNotify(); + /* NOTE: why doesn't the explorer manager reflect changes as made by + * EurekaSelectionModel unless a 'real' explorer view is present -jm + * + * NOTE: the explorer manager was removed when we stopped using it for + * actions since they were diverging from autopsy, however the above + * question is still interesting. -jm */ + // em = ExplorerManager.find(this); + } + + /** initialize the embedded jfx scene */ + public void initScene() { + + if (sceneInited == false) { + Platform.runLater(() -> { + controller = EurekaController.getDefault(); + this.navPanel = new NavPanel(controller); + this.groupPane = new GroupPane(controller); + this.metaDataTable = new MetaDataPane(controller); + + VBox leftPane = new VBox(); + leftPane.getChildren().addAll(navPanel, SummaryTablePane.getDefault()); + + this.splitPane = new SplitPane(); + this.centralStack = new StackPane(splitPane); + + SplitPane.setResizableWithParent(leftPane, Boolean.FALSE); + SplitPane.setResizableWithParent(groupPane, Boolean.TRUE); + SplitPane.setResizableWithParent(metaDataTable, Boolean.FALSE); + splitPane.getItems().addAll(leftPane, groupPane, metaDataTable); + splitPane.setDividerPositions(0.0, 1.0); + + BorderPane borderPane = new BorderPane(centralStack, EurekaToolbar.getDefault(), null, new StatusBar(controller), null); + + this.fullUIStack = new StackPane(borderPane); + EurekaController.getDefault().setStacks(fullUIStack, centralStack); + myScene = new Scene(fullUIStack); + setScene(myScene); + + sceneInited = true; + }); + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/EurekaToolbar.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/EurekaToolbar.fxml new file mode 100644 index 0000000000000000000000000000000000000000..0cee5051111f085f7b252de0eb770dec663442d5 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/EurekaToolbar.fxml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import java.lang.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.image.*?> +<?import javafx.scene.layout.*?> + +<fx:root minWidth="-1.0" orientation="HORIZONTAL" prefWidth="-1.0" type="javafx.scene.control.ToolBar" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> + <items> + <Label text="Group By:"> + <labelFor> + <ComboBox fx:id="groupByBox" editable="false" /> + </labelFor> + </Label> + <fx:reference source="groupByBox" /> + <Region prefHeight="-1.0" prefWidth="20.0" /> + <Label text="Sort By:"> + <labelFor> + <ComboBox fx:id="sortByBox" /> + </labelFor> + </Label> + <HBox id="HBox" fx:id="sortControlGroup" alignment="CENTER" spacing="5.0"> + <children> + <fx:reference source="sortByBox" /> + <VBox alignment="CENTER_LEFT" prefHeight="-1.0" prefWidth="-1.0" spacing="2.0"> + <children> + <RadioButton fx:id="ascRadio" contentDisplay="LEFT" mnemonicParsing="false" selected="true" text="Ascending"> + <graphic> + <ImageView fitHeight="0.0" fitWidth="0.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true"> + <image> + <Image url="@../images/arrow_up.png" /> + </image> + </ImageView> + </graphic> + <toggleGroup> + <ToggleGroup fx:id="orderGroup" /> + </toggleGroup> + </RadioButton> + <RadioButton fx:id="descRadio" contentDisplay="LEFT" mnemonicParsing="false" selected="false" text="Descending" toggleGroup="$orderGroup"> + <graphic> + <ImageView fitHeight="0.0" fitWidth="0.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true"> + <image> + <Image url="@../images/arrow_down.png" /> + </image> + </ImageView> + </graphic> + </RadioButton> + </children> + </VBox> + </children> + </HBox> + <Separator orientation="VERTICAL" prefHeight="-1.0" prefWidth="20.0" /> + <CheckBox fx:id="onlyAnalyzedCheckBox" allowIndeterminate="false" indeterminate="false" mnemonicParsing="false" prefWidth="16.0" selected="true" text="only analyzed groups" underline="false" visible="false" /> + <Label contentDisplay="RIGHT" text="Apply to Selected Files:" textOverrun="ELLIPSIS" /> + <SplitMenuButton id="tagSplitMenu" fx:id="tagSelectedMenuButton" disable="true" mnemonicParsing="false" text="Follow Up" textOverrun="ELLIPSIS"> + <items> + <MenuItem mnemonicParsing="false" text="Action 1" /> + <MenuItem mnemonicParsing="false" text="Action 2" /> + </items> + </SplitMenuButton> + <SplitMenuButton id="catSplitMenu" fx:id="catSelectedMenuButton" disable="true" mnemonicParsing="false" text="Cat-0"> + <items> + <MenuItem mnemonicParsing="false" text="Action 1" /> + <MenuItem mnemonicParsing="false" text="Action 2" /> + </items> + </SplitMenuButton> + <Separator orientation="VERTICAL" prefHeight="-1.0" prefWidth="20.0" /> + <Label text="Thumbnail Size (px):"> + <labelFor> + <Slider fx:id="sizeSlider" blockIncrement="100.0" majorTickUnit="100.0" max="300.0" min="100.0" minorTickCount="0" orientation="HORIZONTAL" prefHeight="-1.0" showTickLabels="true" showTickMarks="true" snapToTicks="true" value="100.0" /> + </labelFor> + </Label> + <fx:reference source="sizeSlider" /> + </items> +</fx:root> diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/EurekaToolbar.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/EurekaToolbar.java new file mode 100644 index 0000000000000000000000000000000000000000..47debdf43334d01a41a20bad912c9710e3384cfc --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/EurekaToolbar.java @@ -0,0 +1,224 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.gui; + +import java.net.URL; +import java.util.ArrayList; +import java.util.ResourceBundle; +import javafx.application.Platform; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.MenuItem; +import javafx.scene.control.RadioButton; +import javafx.scene.control.Slider; +import javafx.scene.control.SplitMenuButton; +import javafx.scene.control.ToggleGroup; +import javafx.scene.control.ToolBar; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; +import javax.swing.SortOrder; +import org.openide.util.Exceptions; +import org.sleuthkit.autopsy.imageanalyzer.EurekaController; +import org.sleuthkit.autopsy.imageanalyzer.FXMLConstructor; +import org.sleuthkit.autopsy.imageanalyzer.FileIDSelectionModel; +import org.sleuthkit.autopsy.imageanalyzer.IconCache; +import org.sleuthkit.autopsy.imageanalyzer.TagUtils; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.Category; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute; +import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupSortBy; +import org.sleuthkit.datamodel.TagName; +import org.sleuthkit.datamodel.TskCoreException; + +/** Controller for the Eureka ToolBar */ +public class EurekaToolbar extends ToolBar { + + private static final int SIZE_SLIDER_DEFAULT = 100; + + @FXML + private ResourceBundle resources; + + @FXML + private URL location; + + @FXML + private ComboBox<DrawableAttribute<?>> groupByBox; + + @FXML + private CheckBox onlyAnalyzedCheckBox; + + @FXML + private Slider sizeSlider; + + @FXML + private ComboBox<GroupSortBy> sortByBox; + + @FXML + private RadioButton ascRadio; + + @FXML + private RadioButton descRadio; + + @FXML + private ToggleGroup orderGroup; + +// @FXML +// private ToggleButton metaDataToggle; + @FXML + private HBox sortControlGroup; + + @FXML + private SplitMenuButton catSelectedMenuButton; + + @FXML + private SplitMenuButton tagSelectedMenuButton; + + private static EurekaToolbar instance; + + private final SimpleObjectProperty<SortOrder> orderProperty = new SimpleObjectProperty<>(SortOrder.ASCENDING); + + private final InvalidationListener queryInvalidationListener = (Observable o) -> { + if (orderGroup.getSelectedToggle() == ascRadio) { + orderProperty.set(SortOrder.ASCENDING); + } else { + orderProperty.set(SortOrder.DESCENDING); + } + + EurekaController.getDefault().getGroupManager().regroup(groupByBox.getSelectionModel().getSelectedItem(), sortByBox.getSelectionModel().getSelectedItem(), getSortOrder(), false); + }; + + synchronized public SortOrder getSortOrder() { + return orderProperty.get(); + } + +// public ReadOnlyBooleanProperty showMetaDataProperty() { +// return metaDataToggle.selectedProperty(); +// } + public DoubleProperty sizeSliderValue() { + return sizeSlider.valueProperty(); + } + + static synchronized public EurekaToolbar getDefault() { + if (instance == null) { + instance = new EurekaToolbar(); + } + return instance; + } + + @FXML + void initialize() { + assert ascRadio != null : "fx:id=\"ascRadio\" was not injected: check your FXML file 'EurekaToolbar.fxml'."; + assert catSelectedMenuButton != null : "fx:id=\"catSelectedMenubutton\" was not injected: check your FXML file 'EurekaToolbar.fxml'."; + assert descRadio != null : "fx:id=\"descRadio\" was not injected: check your FXML file 'EurekaToolbar.fxml'."; + assert groupByBox != null : "fx:id=\"groupByBox\" was not injected: check your FXML file 'EurekaToolbar.fxml'."; + assert onlyAnalyzedCheckBox != null : "fx:id=\"onlyAnalyzedCheckBox\" was not injected: check your FXML file 'EurekaToolbar.fxml'."; + assert orderGroup != null : "fx:id=\"orderGroup\" was not injected: check your FXML file 'EurekaToolbar.fxml'."; + assert sizeSlider != null : "fx:id=\"sizeSlider\" was not injected: check your FXML file 'EurekaToolbar.fxml'."; + assert sortByBox != null : "fx:id=\"sortByBox\" was not injected: check your FXML file 'EurekaToolbar.fxml'."; + assert sortControlGroup != null : "fx:id=\"sortControlGroup\" was not injected: check your FXML file 'EurekaToolbar.fxml'."; + assert tagSelectedMenuButton != null : "fx:id=\"tagSelectedMenubutton\" was not injected: check your FXML file 'EurekaToolbar.fxml'."; + + FileIDSelectionModel.getInstance().getSelected().addListener((Observable o) -> { + tagSelectedMenuButton.setDisable(FileIDSelectionModel.getInstance().getSelected().isEmpty()); + catSelectedMenuButton.setDisable(FileIDSelectionModel.getInstance().getSelected().isEmpty()); + }); + + tagSelectedMenuButton.setOnAction((ActionEvent t) -> { + try { + TagUtils.createSelTagMenuItem(TagUtils.getFollowUpTagName(), tagSelectedMenuButton).getOnAction().handle(t); + } catch (TskCoreException ex) { + Exceptions.printStackTrace(ex); + } + }); + + tagSelectedMenuButton.setGraphic(new ImageView(DrawableAttribute.TAGS.getIcon())); + tagSelectedMenuButton.showingProperty().addListener((ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) -> { + if (t1) { + ArrayList<MenuItem> selTagMenues = new ArrayList<>(); + for (final TagName tn : TagUtils.getNonCategoryTagNames()) { + MenuItem menuItem = TagUtils.createSelTagMenuItem(tn, tagSelectedMenuButton); + selTagMenues.add(menuItem); + } + tagSelectedMenuButton.getItems().setAll(selTagMenues); + } + }); + + catSelectedMenuButton.setOnAction(Category.FIVE.createSelCatMenuItem(catSelectedMenuButton).getOnAction()); + catSelectedMenuButton.setText(Category.FIVE.getDisplayName()); + catSelectedMenuButton.setGraphic(new ImageView(DrawableAttribute.CATEGORY.getIcon())); + catSelectedMenuButton.showingProperty().addListener((ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) -> { + if (t1) { + ArrayList<MenuItem> categoryMenues = new ArrayList<>(); + for (final Category cat : Category.values()) { + MenuItem menuItem = cat.createSelCatMenuItem(catSelectedMenuButton); + categoryMenues.add(menuItem); + } + catSelectedMenuButton.getItems().setAll(categoryMenues); + } + }); + + groupByBox.setItems(FXCollections.observableList(DrawableAttribute.getGroupableAttrs())); + groupByBox.getSelectionModel().select(DrawableAttribute.PATH); + groupByBox.getSelectionModel().selectedItemProperty().addListener(queryInvalidationListener); + groupByBox.disableProperty().bind(EurekaController.getDefault().regroupDisabled()); + groupByBox.setCellFactory((listView) -> new AttributeListCell()); + groupByBox.setButtonCell(new AttributeListCell()); + + sortByBox.setCellFactory((listView) -> new SortByListCell()); + sortByBox.setButtonCell(new SortByListCell()); + sortByBox.setItems(GroupSortBy.getValues()); + + sortByBox.getSelectionModel().selectedItemProperty().addListener(queryInvalidationListener); + + sortByBox.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { + final boolean orderEnabled = newValue == GroupSortBy.NONE || newValue == GroupSortBy.PRIORITY; + ascRadio.setDisable(orderEnabled); + descRadio.setDisable(orderEnabled); + + }); + sortByBox.getSelectionModel().select(GroupSortBy.PRIORITY); +// ascRadio.disableProperty().bind(sortByBox.getSelectionModel().selectedItemProperty().isEqualTo(GroupSortBy.NONE)); +// descRadio.disableProperty().bind(sortByBox.getSelectionModel().selectedItemProperty().isEqualTo(GroupSortBy.NONE)); + + orderGroup.selectedToggleProperty().addListener(queryInvalidationListener); + + IconCache.getDefault().iconSize.bind(sizeSlider.valueProperty()); + + } + + public void reset() { + Platform.runLater(() -> { + groupByBox.getSelectionModel().select(DrawableAttribute.PATH); + sortByBox.getSelectionModel().select(GroupSortBy.NONE); + orderGroup.selectToggle(ascRadio); + sizeSlider.setValue(SIZE_SLIDER_DEFAULT); + }); + } + + private EurekaToolbar() { + FXMLConstructor.construct(this, "EurekaToolbar.fxml"); + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/Fitable.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/Fitable.java new file mode 100644 index 0000000000000000000000000000000000000000..02e846588839f2a2da06ac0a7212126bdd3dafee --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/Fitable.java @@ -0,0 +1,47 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.gui; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.DoubleProperty; + +/** + * + */ +public interface Fitable { + + BooleanProperty preserveRatioProperty(); + + DoubleProperty fitHeightProperty(); + + DoubleProperty fitWidthProperty(); + + boolean isPreserveRatio(); + + double getFitHeight(); + + double getFitWidth(); + + void setPreserveRatio(boolean b); + + void setFitHeight(double d); + + void setFitWidth(double d); + +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/GroupPane.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/GroupPane.fxml new file mode 100644 index 0000000000000000000000000000000000000000..6eb799970d1ef1799510e3644d3bbce3b0c4d755 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/GroupPane.fxml @@ -0,0 +1,136 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import java.lang.*?> +<?import javafx.geometry.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.image.*?> +<?import javafx.scene.layout.*?> +<?import javafx.scene.text.*?> +<?import org.controlsfx.control.*?> + +<fx:root type="BorderPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> + + <center> + <GridView fx:id="gridView" BorderPane.alignment="CENTER" /> + </center> + <bottom> + <BorderPane BorderPane.alignment="CENTER"> + <left> + <HBox alignment="CENTER_LEFT" spacing="5.0" BorderPane.alignment="TOP_LEFT"> + <children> + <Label text="Group Viewing History: " /> + <Button fx:id="backButton" mnemonicParsing="false" text="back"> + <graphic> + <ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true"> + <image> + <Image url="@../images/arrow-180.png" /> + </image> + </ImageView> + </graphic> + </Button> + <Button fx:id="forwardButton" mnemonicParsing="false" text="forward"> + <graphic> + <ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true"> + <image> + <Image url="@../images/arrow.png" /> + </image> + </ImageView> + </graphic> + </Button> + </children> + <BorderPane.margin> + <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> + </BorderPane.margin> + </HBox> + </left> + <right> + <HBox alignment="CENTER_RIGHT" spacing="5.0" BorderPane.alignment="TOP_RIGHT"> + <children> + <Button fx:id="nextButton" contentDisplay="RIGHT" mnemonicParsing="false" text="next unseen group" BorderPane.alignment="CENTER_RIGHT"> + <BorderPane.margin> + <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> + </BorderPane.margin> + <graphic> + <ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true"> + <image> + <Image url="@../images/control-double.png" /> + </image> + </ImageView> + </graphic> + </Button> + </children> + <BorderPane.margin> + <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> + </BorderPane.margin> + </HBox> + </right> + <top> + <Separator prefWidth="200.0" BorderPane.alignment="CENTER"> + <BorderPane.margin> + <Insets bottom="3.0" left="3.0" right="3.0" top="3.0" /> + </BorderPane.margin> + </Separator> + </top> + </BorderPane> + </bottom> + <top> + <VBox> + <children> + <Label fx:id="groupLabel" wrapText="true"> + <font> + <Font size="14.0" /> + </font> + <VBox.margin> + <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> + </VBox.margin> + </Label> + + <ToolBar id="headToolBar" fx:id="headerToolBar" maxHeight="-Infinity" maxWidth="-1.0" minHeight="-Infinity" minWidth="-1.0" prefHeight="-1.0" prefWidth="-1.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" BorderPane.alignment="CENTER" VBox.vgrow="NEVER"> + <items> + <Label text="Apply to Group:" /> + <SplitMenuButton fx:id="grpTagSplitMenu" mnemonicParsing="false" text="Follow Up"> + <items> + <MenuItem mnemonicParsing="false" text="Action 1" /> + <MenuItem mnemonicParsing="false" text="Action 2" /> + </items> + </SplitMenuButton> + <SplitMenuButton fx:id="grpCatSplitMenu" mnemonicParsing="false" text="Cat-0"> + <items> + <MenuItem mnemonicParsing="false" text="Action 1" /> + <MenuItem mnemonicParsing="false" text="Action 2" /> + </items> + </SplitMenuButton> + <Region fx:id="spacer" prefHeight="-1.0" prefWidth="-1.0" /> + <Separator prefWidth="30.0" /> + <SegmentedButton fx:id="segButton"> + <buttons> + <ToggleButton id="" fx:id="tileToggle" alignment="CENTER" contentDisplay="GRAPHIC_ONLY" graphicTextGap="0.0" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="32.0" mnemonicParsing="false" prefWidth="-1.0" scaleX="1.0" selected="true" text="" textAlignment="CENTER"> + <graphic> + <ImageView fitHeight="16.0" fitWidth="16.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true" translateY="1.0"> + <image> + <Image url="@../images/application_view_tile.png" /> + </image> + </ImageView> + </graphic> + </ToggleButton> + <ToggleButton id="filmStripToggle" fx:id="slideShowToggle" contentDisplay="GRAPHIC_ONLY" graphicTextGap="0.0" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="32.0" mnemonicParsing="false" prefWidth="-1.0" text=""> + <graphic> + <ImageView fitHeight="16.0" fitWidth="16.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true" rotate="90.0" translateY="1.0"> + <image> + <Image url="@../images/film.png" /> + </image> + </ImageView> + </graphic> + </ToggleButton> + </buttons> + </SegmentedButton> + + </items> + </ToolBar> + </children> + </VBox> + </top> + <padding> + <Insets bottom="1.0" left="1.0" right="1.0" top="1.0" /> + </padding> +</fx:root> diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/GroupPane.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/GroupPane.java new file mode 100644 index 0000000000000000000000000000000000000000..373c4d603c3899cc19ef866eaba296eb5982e15d --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/GroupPane.java @@ -0,0 +1,780 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.gui; + +import com.google.common.collect.ImmutableMap; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.logging.Level; +import java.util.stream.IntStream; +import javafx.animation.Interpolator; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.beans.Observable; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.fxml.FXML; +import javafx.geometry.Bounds; +import javafx.scene.control.Button; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.Label; +import javafx.scene.control.MenuItem; +import javafx.scene.control.ScrollBar; +import javafx.scene.control.SplitMenuButton; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.ToolBar; +import javafx.scene.effect.DropShadow; +import javafx.scene.image.ImageView; +import javafx.scene.input.KeyCode; +import static javafx.scene.input.KeyCode.DIGIT0; +import static javafx.scene.input.KeyCode.DIGIT1; +import static javafx.scene.input.KeyCode.DIGIT2; +import static javafx.scene.input.KeyCode.DIGIT3; +import static javafx.scene.input.KeyCode.DIGIT4; +import static javafx.scene.input.KeyCode.DIGIT5; +import static javafx.scene.input.KeyCode.DOWN; +import static javafx.scene.input.KeyCode.LEFT; +import static javafx.scene.input.KeyCode.NUMPAD0; +import static javafx.scene.input.KeyCode.NUMPAD1; +import static javafx.scene.input.KeyCode.NUMPAD2; +import static javafx.scene.input.KeyCode.NUMPAD3; +import static javafx.scene.input.KeyCode.NUMPAD4; +import static javafx.scene.input.KeyCode.NUMPAD5; +import static javafx.scene.input.KeyCode.RIGHT; +import static javafx.scene.input.KeyCode.UP; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.TilePane; +import javafx.scene.paint.Color; +import javafx.util.Duration; +import javax.swing.Action; +import javax.swing.SwingUtilities; +import org.apache.commons.lang3.StringUtils; +import org.controlsfx.control.GridCell; +import org.controlsfx.control.GridView; +import org.controlsfx.control.SegmentedButton; +import org.controlsfx.control.action.ActionUtils; +import org.openide.util.Exceptions; +import org.openide.util.Lookup; +import org.openide.util.actions.Presenter; +import org.openide.windows.TopComponent; +import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.corecomponentinterfaces.ContextMenuActionsProvider; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.directorytree.ExtractAction; +import org.sleuthkit.autopsy.imageanalyzer.EurekaController; +import org.sleuthkit.autopsy.imageanalyzer.EurekaTopComponent; +import org.sleuthkit.autopsy.imageanalyzer.FXMLConstructor; +import org.sleuthkit.autopsy.imageanalyzer.FileIDSelectionModel; +import org.sleuthkit.autopsy.imageanalyzer.TagUtils; +import org.sleuthkit.autopsy.imageanalyzer.ThreadConfined; +import org.sleuthkit.autopsy.imageanalyzer.ThreadConfined.ThreadType; +import org.sleuthkit.autopsy.imageanalyzer.actions.AddDrawableTagAction; +import org.sleuthkit.autopsy.imageanalyzer.actions.Back; +import org.sleuthkit.autopsy.imageanalyzer.actions.CategorizeAction; +import org.sleuthkit.autopsy.imageanalyzer.actions.Forward; +import org.sleuthkit.autopsy.imageanalyzer.actions.NextUnseenGroup; +import org.sleuthkit.autopsy.imageanalyzer.actions.SwingMenuItemAdapter; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.Category; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute; +import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupViewMode; +import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupViewState; +import org.sleuthkit.autopsy.imageanalyzer.grouping.Grouping; +import org.sleuthkit.datamodel.TagName; +import org.sleuthkit.datamodel.TskCoreException; + +/** A GroupPane displays the contents of a {@link Grouping}. It support both a + * {@link TilePane} based view and a {@link SlideShowView} view by swapping out + * its internal components. + * + * TODO: review for synchronization issues. + * TODO: Extract the The TilePane instance to a separate class analogous to + * the SlideShow */ +public class GroupPane extends BorderPane implements GroupView { + + private static final Logger LOGGER = Logger.getLogger(GroupPane.class.getName()); + + private static final DropShadow DROP_SHADOW = new DropShadow(10, Color.BLUE); + + private static final Timeline flashAnimation = new Timeline(new KeyFrame(Duration.millis(400), new KeyValue(DROP_SHADOW.radiusProperty(), 1, Interpolator.LINEAR)), + new KeyFrame(Duration.millis(400), new KeyValue(DROP_SHADOW.radiusProperty(), 15, Interpolator.LINEAR)) + ); + + private static final FileIDSelectionModel globalSelectionModel = FileIDSelectionModel.getInstance(); + + private final Back backAction; + + private final Forward forwardAction; + + @FXML + private URL location; + + @FXML + private ResourceBundle resources; + + @FXML + private SplitMenuButton grpCatSplitMenu; + + @FXML + private SplitMenuButton grpTagSplitMenu; + + @FXML + private ToolBar headerToolBar; + + @FXML + private SegmentedButton segButton; + + private SlideShowView slideShowPane; + + @FXML + private ToggleButton slideShowToggle; + + @FXML + private Region spacer; + + @FXML + private GridView<Long> gridView; + + @FXML + private ToggleButton tileToggle; + + @FXML + private Button nextButton; + + @FXML + private Button backButton; + + @FXML + private Button forwardButton; + + @FXML + private Label groupLabel; + + private final KeyboardHandler tileKeyboardNavigationHandler = new KeyboardHandler(); + + private final NextUnseenGroup nextGroupAction; + + private final EurekaController controller; + + private ContextMenu contextMenu; + + private Integer selectionAnchorIndex; + + /** the current GroupViewMode of this GroupPane */ + private final SimpleObjectProperty<GroupViewMode> groupViewMode = new SimpleObjectProperty<>(GroupViewMode.TILE); + + /** the grouping this pane is currently the view for */ + private final ReadOnlyObjectWrapper<Grouping> grouping = new ReadOnlyObjectWrapper<>(); + + /** map from fileIDs to their assigned cells in the tile view. This is used + * to determine whether fileIDs are visible or are offscreen. No entry + * indicates the given fileID is not displayed on screenDrawableCells + * responsible for adding and removing themselves from this map */ + @ThreadConfined(type = ThreadType.UI) + private final Map<Long, DrawableCell> cellMap = new HashMap<>(); + + public GroupPane(EurekaController controller) { + this.controller = controller; + nextGroupAction = new NextUnseenGroup(controller); + backAction = new Back(controller); + forwardAction = new Forward(controller); + FXMLConstructor.construct(this, "GroupPane.fxml"); + } + + public void activateSlideShowViewer(Long slideShowFileId) { + groupViewMode.set(GroupViewMode.SLIDE_SHOW); + gridView.removeEventHandler(KeyEvent.KEY_PRESSED, tileKeyboardNavigationHandler); + //make a new slideShowPane if necessary + if (slideShowPane == null) { + slideShowPane = new SlideShowView(this); + } + + //assign last selected file or if none first file in group + if (slideShowFileId == null || grouping.get().fileIds().contains(slideShowFileId) == false) { + slideShowFileId = grouping.get().fileIds().get(0); + } + + slideShowPane.setFile(slideShowFileId); + setCenter(slideShowPane); + } + + public void activateTileViewer() { + groupViewMode.set(GroupViewMode.TILE); + gridView.addEventHandler(KeyEvent.KEY_PRESSED, tileKeyboardNavigationHandler); + setCenter(gridView); + this.scrollToFileID(globalSelectionModel.lastSelectedProperty().get()); + } + + public Grouping getGrouping() { + return grouping.get(); + } + + /** + * @return the text to display as part of the header + */ + public String getHeaderString() { + int size = grouping.get().getSize(); + int hashHitCount = grouping.get().getFilesWithHashSetHitsCount(); + String groupName; + if (grouping.get().groupKey.getAttribute() == DrawableAttribute.TAGS) { + groupName = ((TagName) grouping.get().groupKey.getValue()).getDisplayName(); + } else { + groupName = grouping.get().groupKey.getValue().toString(); + } + return StringUtils.defaultIfBlank(groupName, Grouping.UNKNOWN) + " -- " + hashHitCount + " hash set hits / " + size + " files"; + } + + private MenuItem createGrpCatMenuItem(final Category cat) { + final MenuItem menuItem = new MenuItem(cat.getDisplayName(), new ImageView(DrawableAttribute.CATEGORY.getIcon())); + menuItem.setOnAction(new EventHandler<ActionEvent>() { + @Override + public void handle(ActionEvent t) { + selectAllFiles(); + new CategorizeAction().addTag(cat.getTagName(), ""); + + grpCatSplitMenu.setText(cat.getDisplayName()); + grpCatSplitMenu.setOnAction(this); + } + }); + return menuItem; + } + + private MenuItem createGrpTagMenuItem(final TagName tn) { + final MenuItem menuItem = new MenuItem(tn.getDisplayName(), new ImageView(DrawableAttribute.TAGS.getIcon())); + menuItem.setOnAction(new EventHandler<ActionEvent>() { + @Override + public void handle(ActionEvent t) { + selectAllFiles(); + AddDrawableTagAction.getInstance().addTag(tn, ""); + + grpTagSplitMenu.setText(tn.getDisplayName()); + grpTagSplitMenu.setOnAction(this); + } + }); + return menuItem; + } + + private void selectAllFiles() { + globalSelectionModel.clearAndSelectAll(getGrouping().fileIds()); + } + + /** reset the text and icons to represent the currently filtered files */ + protected void resetHeaderString() { + int size = grouping.get().getSize(); + int hashHitCount = grouping.get().getFilesWithHashSetHitsCount(); + String groupName; + if (grouping.get().groupKey.getAttribute() == DrawableAttribute.TAGS) { + groupName = ((TagName) grouping.get().groupKey.getValue()).getDisplayName(); + } else { + groupName = grouping.get().groupKey.getValue().toString(); + } + groupLabel.setText(StringUtils.defaultIfBlank(groupName, Grouping.UNKNOWN) + " -- " + hashHitCount + " hash set hits / " + size + " files"); + } + + ContextMenu getContextMenu() { + return contextMenu; + } + + ReadOnlyObjectProperty<Grouping> grouping() { + return grouping.getReadOnlyProperty(); + } + + /** called automatically during constructor by FXMLConstructor. + * + * checks that FXML loading went ok and performs additional setup */ + @FXML + void initialize() { + assert gridView != null : "fx:id=\"tilePane\" was not injected: check your FXML file 'GroupPane.fxml'."; + assert grpCatSplitMenu != null : "fx:id=\"grpCatSplitMenu\" was not injected: check your FXML file 'GroupHeader.fxml'."; + assert grpTagSplitMenu != null : "fx:id=\"grpTagSplitMenu\" was not injected: check your FXML file 'GroupHeader.fxml'."; + assert headerToolBar != null : "fx:id=\"headerToolBar\" was not injected: check your FXML file 'GroupHeader.fxml'."; + assert segButton != null : "fx:id=\"previewList\" was not injected: check your FXML file 'GroupHeader.fxml'."; + assert slideShowToggle != null : "fx:id=\"segButton\" was not injected: check your FXML file 'GroupHeader.fxml'."; + assert tileToggle != null : "fx:id=\"tileToggle\" was not injected: check your FXML file 'GroupHeader.fxml'."; + + grouping.addListener((o) -> { + //when the assigned group changes, reset the scroll ar to the top + getScrollBar().ifPresent((scrollBar) -> { + scrollBar.setValue(0); + }); + //and assign fileIDs to gridView + gridView.setItems(grouping.get().fileIds()); + }); + + //configure flashing glow animation on next unseen group button + flashAnimation.setCycleCount(Timeline.INDEFINITE); + flashAnimation.setAutoReverse(true); + + //configure gridView cell properties + gridView.cellHeightProperty().bind(EurekaToolbar.getDefault().sizeSliderValue().add(75)); + gridView.cellWidthProperty().bind(EurekaToolbar.getDefault().sizeSliderValue().add(75)); + gridView.setCellFactory((GridView<Long> param) -> new DrawableCell()); + + //configure toolbar properties + HBox.setHgrow(spacer, Priority.ALWAYS); + spacer.setMinWidth(Region.USE_PREF_SIZE); + + ArrayList<MenuItem> grpTagMenues = new ArrayList<>(); + for (final TagName tn : TagUtils.getNonCategoryTagNames()) { + MenuItem menuItem = createGrpTagMenuItem(tn); + grpTagMenues.add(menuItem); + } + try { + grpTagSplitMenu.setText(TagUtils.getFollowUpTagName().getDisplayName()); + grpTagSplitMenu.setOnAction(createGrpTagMenuItem(TagUtils.getFollowUpTagName()).getOnAction()); + } catch (TskCoreException tskCoreException) { + LOGGER.log(Level.WARNING, "failed to load FollowUpTagName", tskCoreException.getLocalizedMessage()); + Exceptions.printStackTrace(tskCoreException); + } + grpTagSplitMenu.setGraphic(new ImageView(DrawableAttribute.TAGS.getIcon())); + grpTagSplitMenu.getItems().setAll(grpTagMenues); + + ArrayList<MenuItem> grpCategoryMenues = new ArrayList<>(); + for (final Category cat : Category.values()) { + MenuItem menuItem = createGrpCatMenuItem(cat); + grpCategoryMenues.add(menuItem); + } + grpCatSplitMenu.setText(Category.FIVE.getDisplayName()); + grpCatSplitMenu.setGraphic(new ImageView(DrawableAttribute.CATEGORY.getIcon())); + grpCatSplitMenu.getItems().setAll(grpCategoryMenues); + grpCatSplitMenu.setOnAction(createGrpCatMenuItem(Category.FIVE).getOnAction()); + + Runnable syncMode = () -> { + switch (groupViewMode.get()) { + case SLIDE_SHOW: + slideShowToggle.setSelected(true); + break; + case TILE: + tileToggle.setSelected(true); + break; + } + }; + syncMode.run(); + //make togle states match view state + groupViewMode.addListener((o) -> { + syncMode.run(); + }); + + slideShowToggle.toggleGroupProperty().addListener((o) -> { + slideShowToggle.getToggleGroup().selectedToggleProperty().addListener((observable, oldToggle, newToggle) -> { + if (newToggle == null) { + oldToggle.setSelected(true); + } + }); + }); + + //listen to toggles and update view state + slideShowToggle.setOnAction((ActionEvent t) -> { + activateSlideShowViewer(globalSelectionModel.lastSelectedProperty().get()); + }); + + tileToggle.setOnAction((ActionEvent t) -> { + activateTileViewer(); + }); + + controller.viewState().addListener((ObservableValue<? extends GroupViewState> observable, GroupViewState oldValue, GroupViewState newValue) -> { + setViewState(newValue); + }); + + gridView.addEventHandler(KeyEvent.KEY_PRESSED, tileKeyboardNavigationHandler); + gridView.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() { + + private ContextMenu buildContextMenu() { + ArrayList<MenuItem> menuItems = new ArrayList<>(); + + menuItems.add(CategorizeAction.getPopupMenu()); + + menuItems.add(AddDrawableTagAction.getInstance().getPopupMenu()); + + Collection<? extends ContextMenuActionsProvider> menuProviders = Lookup.getDefault().lookupAll(ContextMenuActionsProvider.class); + + for (ContextMenuActionsProvider provider : menuProviders) { + + for (final Action act : provider.getActions()) { + + if (act instanceof Presenter.Popup) { + Presenter.Popup aact = (Presenter.Popup) act; + + menuItems.add(SwingMenuItemAdapter.create(aact.getPopupPresenter())); + } + } + } + final MenuItem extractMenuItem = new MenuItem("Extract File(s)"); + extractMenuItem.setOnAction((ActionEvent t) -> { + SwingUtilities.invokeLater(() -> { + TopComponent etc = WindowManager.getDefault().findTopComponent(EurekaTopComponent.PREFERRED_ID); + ExtractAction.getInstance().actionPerformed(new java.awt.event.ActionEvent(etc, 0, null)); + }); + }); + menuItems.add(extractMenuItem); + + ContextMenu contextMenu = new ContextMenu(menuItems.toArray(new MenuItem[]{})); + contextMenu.setAutoHide(true); + return contextMenu; + } + + @Override + public void handle(MouseEvent t) { + switch (t.getButton()) { + case PRIMARY: + if (t.getClickCount() == 1) { + globalSelectionModel.clearSelection(); + if (contextMenu != null) { + contextMenu.hide(); + } + } + t.consume(); + break; + case SECONDARY: + if (t.getClickCount() == 1) { + selectAllFiles(); + } + if (contextMenu == null) { + contextMenu = buildContextMenu(); + } + + contextMenu.hide(); + contextMenu.show(GroupPane.this, t.getScreenX(), t.getScreenY()); + t.consume(); + break; + } + } + }); + + ActionUtils.configureButton(nextGroupAction, nextButton); + final EventHandler<ActionEvent> onAction = nextButton.getOnAction(); + nextButton.setOnAction((ActionEvent event) -> { + flashAnimation.stop(); + nextButton.setEffect(null); + onAction.handle(event); + }); + + ActionUtils.configureButton(forwardAction, forwardButton); + ActionUtils.configureButton(backAction, backButton); + + nextGroupAction.disabledProperty().addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> { + nextButton.setEffect(newValue ? null : DROP_SHADOW); + if (newValue == false) { + flashAnimation.play(); + } else { + flashAnimation.stop(); + } + }); + + //listen to tile selection and make sure it is visible in scroll area + //TODO: make sure we are testing complete visability not just bounsd intersection + globalSelectionModel.lastSelectedProperty().addListener((observable, oldFileID, newFileId) -> { + if (groupViewMode.get() == GroupViewMode.SLIDE_SHOW) { + slideShowPane.setFile(newFileId); + } else { + + scrollToFileID(newFileId); + } + }); + + setViewState(controller.viewState().get()); + } + + @ThreadConfined(type = ThreadType.UI) + private void scrollToFileID(final Long newFileID) { + if (newFileID == null) { + //scrolling to no file doesn't make sense, so abort. + return; + } + + int selectedIndex = grouping.get().fileIds().indexOf(newFileID); + + if (selectedIndex == -1) { + //somehow we got passed a file id that isn't in the curent group. + //this should never happen, but if it does everything is going to fail, so abort. + return; + } + + Optional<ScrollBar> scrollBarOptional = getScrollBar(); + scrollBarOptional.ifPresent((ScrollBar scrollBar) -> { + DrawableCell cell = cellMap.get(newFileID); + + //while there is no tile/cell for the given id, scroll based on index in group + while (cell == null) { + Integer minIndex = cellMap.keySet().stream() + .map(grouping.get().fileIds()::indexOf) + .min(Integer::compare).get(); + Integer maxIndex = cellMap.keySet().stream() + .map(grouping.get().fileIds()::indexOf) + .max(Integer::compare).get(); + + if (selectedIndex < minIndex) { + scrollBar.decrement(); + } else if (selectedIndex > maxIndex) { + scrollBar.increment(); + } else { + //sometimes the cellMap isn't up to date, so move the position arbitrarily to update the cellMap + //TODO: this is clunky and slow, find a better way to do this + scrollBar.adjustValue(.5); + } + cell = cellMap.get(newFileID); + + } + + final Bounds gridViewBounds = gridView.localToScene(gridView.getBoundsInLocal()); + + Bounds tileBounds = cell.localToScene(cell.getBoundsInLocal()); + + //while the cell is not within the visisble bounds of the gridview, scroll based on screen coordinates + int i = 0; + + while (gridViewBounds.contains(tileBounds) + == false && (i++ < 100)) { + + if (tileBounds.getMinY() < gridViewBounds.getMinY()) { + scrollBar.decrement(); + } else if (tileBounds.getMaxY() > gridViewBounds.getMaxY()) { + scrollBar.increment(); + } + tileBounds = cell.localToScene(cell.getBoundsInLocal()); + } + }); + } + + /** + * assigns a grouping for this pane to represent and initializes grouping + * specific properties and listeners + * + * @param grouping the new grouping assigned to this group + */ + void setViewState(GroupViewState viewState) { + if (viewState == null) { + setCenter(null); + groupLabel.setText(null); + } else { + if (this.grouping.get() != viewState.getGroup()) { + this.grouping.set(viewState.getGroup()); + //set the embeded header + resetHeaderString(); + grouping.get().fileIds().addListener((Observable o) -> { + resetHeaderString(); + }); + + } + + if (viewState.getMode() == GroupViewMode.TILE) { + activateTileViewer(); + } else { + activateSlideShowViewer(viewState.getSlideShowfileID().orElse(null)); + + } + } + } + + private class DrawableCell extends GridCell<Long> { + + private final DrawableTile tile = new DrawableTile(GroupPane.this); + + public DrawableCell() { + itemProperty().addListener((ObservableValue<? extends Long> observable, Long oldValue, Long newValue) -> { + if (oldValue != null) { + cellMap.remove(oldValue, DrawableCell.this); + } + if (newValue != null) { + cellMap.put(newValue, DrawableCell.this); + } + }); + + setGraphic(tile); + } + + @Override + protected void updateItem(Long item, boolean empty) { + super.updateItem(item, empty); + tile.setFile(item); + } + } + + /** + * implements the key handler for tile navigation ( up, down , left, right + * arrows) + */ + private class KeyboardHandler implements EventHandler<KeyEvent> { + + @Override + public void handle(KeyEvent t) { + + if (t.getEventType() == KeyEvent.KEY_PRESSED) { + if (t.getCode() == KeyCode.SHIFT) { + if (selectionAnchorIndex == null) { + selectionAnchorIndex = grouping.get().fileIds().indexOf(globalSelectionModel.lastSelectedProperty().get()); + } + } + if (Arrays.asList(KeyCode.UP, KeyCode.DOWN, KeyCode.LEFT, KeyCode.RIGHT).contains(t.getCode())) { + handleArrows(t); + } + if (t.getCode() == KeyCode.PAGE_DOWN) { + getScrollBar().ifPresent((scrollBar) -> { + scrollBar.adjustValue(1); + }); + } + + if (t.getCode() == KeyCode.PAGE_UP) { + getScrollBar().ifPresent((scrollBar) -> { + scrollBar.adjustValue(0); + }); + } + if (t.getCode() == KeyCode.ENTER) { + nextGroupAction.handle(null); + } + + if (t.isAltDown()) { + switch (t.getCode()) { + case NUMPAD0: + case DIGIT0: + selectAllFiles(); + new CategorizeAction().addTag(Category.FIVE.getTagName(), ""); + break; + case NUMPAD1: + case DIGIT1: + selectAllFiles(); + new CategorizeAction().addTag(Category.ONE.getTagName(), ""); + break; + case NUMPAD2: + case DIGIT2: + selectAllFiles(); + new CategorizeAction().addTag(Category.TWO.getTagName(), ""); + break; + case NUMPAD3: + case DIGIT3: + selectAllFiles(); + new CategorizeAction().addTag(Category.THREE.getTagName(), ""); + break; + case NUMPAD4: + case DIGIT4: + selectAllFiles(); + new CategorizeAction().addTag(Category.FOUR.getTagName(), ""); + break; + case NUMPAD5: + case DIGIT5: + selectAllFiles(); + new CategorizeAction().addTag(Category.ZERO.getTagName(), ""); + break; + } + } else { + + switch (t.getCode()) { + case NUMPAD0: + case DIGIT0: + new CategorizeAction().addTag(Category.ZERO.getTagName(), ""); + break; + case NUMPAD1: + case DIGIT1: + new CategorizeAction().addTag(Category.ONE.getTagName(), ""); + break; + case NUMPAD2: + case DIGIT2: + new CategorizeAction().addTag(Category.TWO.getTagName(), ""); + break; + case NUMPAD3: + case DIGIT3: + new CategorizeAction().addTag(Category.THREE.getTagName(), ""); + break; + case NUMPAD4: + case DIGIT4: + new CategorizeAction().addTag(Category.FOUR.getTagName(), ""); + break; + case NUMPAD5: + case DIGIT5: + new CategorizeAction().addTag(Category.FIVE.getTagName(), ""); + break; + } + } + } + + t.consume(); + } + + private Integer getPrefColumns() { + return Math.max((int) Math.floor((gridView.getWidth() - 18) / (gridView.getCellWidth() + gridView.getHorizontalCellSpacing() * 2)), 1); + } + + private void handleArrows(KeyEvent t) { + Long lastSelectFileId = globalSelectionModel.lastSelectedProperty().get(); + + int lastSelectedIndex = lastSelectFileId != null + ? grouping.get().fileIds().indexOf(lastSelectFileId) + : Optional.ofNullable(selectionAnchorIndex).orElse(0); + + final Integer columns = getPrefColumns(); + + final Map<KeyCode, Integer> tileIndexMap = ImmutableMap.of(UP, -columns, DOWN, columns, LEFT, -1, RIGHT, 1); + + // implement proper keyboard based multiselect + int indexOfToBeSelectedTile = lastSelectedIndex + tileIndexMap.get(t.getCode()); + final int size = grouping.get().fileIds().size(); + if (0 > indexOfToBeSelectedTile) { + //don't select past begining of group + } else if (0 <= indexOfToBeSelectedTile && indexOfToBeSelectedTile < size) { + //normal selection within group + makeSelection(t.isShiftDown(), grouping.get().fileIds().get(indexOfToBeSelectedTile)); + } else if (indexOfToBeSelectedTile <= size - 1 + columns - (size % columns)) { + //selection last item if selection is empty space at end of group + makeSelection(t.isShiftDown(), grouping.get().fileIds().get(size - 1)); + } else { + //don't select past end of group + } + } + } + + private Optional<ScrollBar> getScrollBar() { + if (gridView == null || gridView.getSkin() == null) { + return Optional.empty(); + } + return Optional.ofNullable((ScrollBar) gridView.getSkin().getNode().lookup(".scroll-bar")); + } + + void makeSelection(Boolean shiftDown, Long newFileID) { + + if (shiftDown) { + //TODO: do more hear to implement slicker multiselect + int endIndex = grouping.get().fileIds().indexOf(newFileID); + int startIndex = IntStream.of(grouping.get().fileIds().size(), selectionAnchorIndex, endIndex).min().getAsInt(); + endIndex = IntStream.of(0, selectionAnchorIndex, endIndex).max().getAsInt(); + List<Long> subList = grouping.get().fileIds().subList(startIndex, endIndex + 1); + + globalSelectionModel.clearAndSelectAll(subList.toArray(new Long[subList.size()])); + globalSelectionModel.select(newFileID); + } else { + selectionAnchorIndex = null; + globalSelectionModel.clearAndSelect(newFileID); + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/GroupView.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/GroupView.java new file mode 100644 index 0000000000000000000000000000000000000000..456d02970266e7ae269103059a046dfde1691ba4 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/GroupView.java @@ -0,0 +1,8 @@ + +package org.sleuthkit.autopsy.imageanalyzer.gui; + + + +public interface GroupView { + +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/MediaControl.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/MediaControl.fxml new file mode 100644 index 0000000000000000000000000000000000000000..c872afc98ed1a3cbdb5588f773644cc3156698ca --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/MediaControl.fxml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import java.lang.*?> +<?import java.util.*?> +<?import javafx.geometry.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.layout.*?> +<?import javafx.scene.paint.*?> + +<fx:root type="javafx.scene.layout.BorderPane" id="BorderPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2"> + <bottom> + <HBox fillHeight="true" prefHeight="-1.0" prefWidth="-1.0" style="-fx-background-color: linear-gradient(to bottom, derive(-fx-base,-30%), derive(-fx-base,-60%)), linear-gradient(to bottom, derive(-fx-base,65%) 2%, derive(-fx-base,-20%) 95%); "> + <children> + <Button id="controlbutton" fx:id="controlButton" contentDisplay="TEXT_ONLY" mnemonicParsing="false" prefHeight="-1.0" prefWidth="-1.0" text=">" HBox.hgrow="NEVER" /> + <Region prefHeight="16.0" prefWidth="16.0" HBox.hgrow="NEVER" /> + <Label text="Time:" HBox.hgrow="NEVER" /> + <Slider fx:id="timeSlider" prefHeight="21.0" prefWidth="315.0" HBox.hgrow="ALWAYS" /> + <Label id="timeLable" fx:id="timeLabel" text="Label" HBox.hgrow="NEVER" /> + <Region prefHeight="16.0" prefWidth="16.0" HBox.hgrow="NEVER" /> + <Label text="Volume:" visible="false" HBox.hgrow="NEVER" /> + <Slider fx:id="volumeSlider" prefHeight="21.0" prefWidth="100.0" visible="false" HBox.hgrow="SOMETIMES" /> + </children> + <padding> + <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> + </padding> + </HBox> + </bottom> +</fx:root> diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/MediaControl.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/MediaControl.java new file mode 100644 index 0000000000000000000000000000000000000000..a822acb00a2055b178f2c3462f623b8754648a75 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/MediaControl.java @@ -0,0 +1,300 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.gui; + +import org.sleuthkit.autopsy.imageanalyzer.FXMLConstructor; +import java.net.URL; +import java.util.ResourceBundle; +import java.util.logging.Level; +import javafx.application.Platform; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.DoubleProperty; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.Slider; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.Region; +import javafx.scene.media.MediaPlayer; +import javafx.scene.media.MediaPlayer.Status; +import javafx.scene.media.MediaView; +import javafx.util.Duration; +import org.sleuthkit.autopsy.coreutils.Logger; + +public class MediaControl extends BorderPane implements Fitable { + + private MediaPlayer mp; + + private MediaView mediaView; + + private final boolean repeat = false; + + private boolean stopRequested = false; + + private boolean atEndOfMedia = false; + + private Duration duration; + + @FXML + private ResourceBundle resources; + + @FXML + private URL location; + + @FXML + private Button controlButton; + + @FXML + private Label timeLabel; + + @FXML + private Slider timeSlider; + + @FXML + private Slider volumeSlider; + + @FXML + void initialize() { + assert controlButton != null : "fx:id=\"controlButton\" was not injected: check your FXML file 'MediaControl.fxml'."; + assert timeLabel != null : "fx:id=\"timeLabel\" was not injected: check your FXML file 'MediaControl.fxml'."; + assert timeSlider != null : "fx:id=\"timeSlider\" was not injected: check your FXML file 'MediaControl.fxml'."; + assert volumeSlider != null : "fx:id=\"volumeSlider\" was not injected: check your FXML file 'MediaControl.fxml'."; + + mediaView = new MediaView(mp); + mediaView.setPreserveRatio(true); + mediaView.fitHeightProperty().bind(this.heightProperty().subtract(50)); + mediaView.fitWidthProperty().bind(this.widthProperty()); + setCenter(mediaView); + + controlButton.setOnAction(new EventHandler<ActionEvent>() { + public void handle(ActionEvent e) { + try { + Status status = mp.getStatus(); + + if (status == Status.UNKNOWN || status == Status.HALTED) { + // don't do anything in these states + return; + } + + if (status == Status.PAUSED + || status == Status.READY + || status == Status.STOPPED) { + // rewind the movie if we're sitting at the end + if (atEndOfMedia) { + mp.seek(mp.getStartTime()); + atEndOfMedia = false; + } + mp.play(); + } else { + mp.pause(); + } + } catch (Exception ex) { + Logger.getAnonymousLogger().log(Level.SEVERE, "message", ex); + } + } + }); + mp.currentTimeProperty().addListener(new InvalidationListener() { + public void invalidated(Observable ov) { + updateValues(); + } + }); + + mp.setOnPlaying(new Runnable() { + public void run() { + if (stopRequested) { + mp.pause(); + stopRequested = false; + } else { + controlButton.setText("||"); + } + } + }); + + mp.setOnPaused(new Runnable() { + public void run() { + System.out.println("onPaused"); + controlButton.setText(">"); + } + }); + + mp.setOnReady(new Runnable() { + public void run() { + duration = mp.getMedia().getDuration(); + updateValues(); + } + }); + + mp.setCycleCount(repeat ? MediaPlayer.INDEFINITE : 1); + mp.setOnEndOfMedia(new Runnable() { + public void run() { + if (!repeat) { + controlButton.setText(">"); + stopRequested = true; + atEndOfMedia = true; + } + } + }); + + // Add time slider + timeSlider.setMinWidth(50); + timeSlider.setMaxWidth(Double.MAX_VALUE); + timeSlider.valueProperty().addListener(new InvalidationListener() { + public void invalidated(Observable ov) { + if (timeSlider.isValueChanging()) { + // multiply duration by percentage calculated by slider position + mp.seek(duration.multiply(timeSlider.getValue() / 100.0)); + } + } + }); + + // Add Volume slider + volumeSlider.setPrefWidth(70); + volumeSlider.setMaxWidth(Region.USE_PREF_SIZE); + volumeSlider.setMinWidth(30); + volumeSlider.valueProperty().addListener(new InvalidationListener() { + public void invalidated(Observable ov) { + if (volumeSlider.isValueChanging()) { + mp.setVolume(volumeSlider.getValue() / 100.0); + } + } + }); + + } + + public MediaControl(final MediaPlayer mp) { + this.mp = mp; + + FXMLConstructor.construct(this, "MediaControl.fxml"); + } + + protected void updateValues() { + if (timeLabel != null && timeSlider != null && volumeSlider != null) { + Platform.runLater(new Runnable() { + public void run() { + Duration currentTime = mp.getCurrentTime(); + timeLabel.setText(formatTime(currentTime, duration)); + timeSlider.setDisable(duration.isUnknown()); + if (!timeSlider.isDisabled() + && duration.greaterThan(Duration.ZERO) + && !timeSlider.isValueChanging()) { + timeSlider.setValue(currentTime.divide(duration).toMillis() + * 100.0); + } + if (!volumeSlider.isValueChanging()) { + volumeSlider.setValue((int) Math.round(mp.getVolume() + * 100)); + } + } + }); + } + } + + private static String formatTime(Duration elapsed, Duration duration) { + int intElapsed = (int) Math.floor(elapsed.toSeconds()); + int elapsedHours = intElapsed / (60 * 60); + if (elapsedHours > 0) { + intElapsed -= elapsedHours * 60 * 60; + } + int elapsedMinutes = intElapsed / 60; + int elapsedSeconds = intElapsed - elapsedHours * 60 * 60 + - elapsedMinutes * 60; + + if (duration.greaterThan(Duration.ZERO)) { + int intDuration = (int) Math.floor(duration.toSeconds()); + int durationHours = intDuration / (60 * 60); + if (durationHours > 0) { + intDuration -= durationHours * 60 * 60; + } + int durationMinutes = intDuration / 60; + int durationSeconds = intDuration - durationHours * 60 * 60 + - durationMinutes * 60; + if (durationHours > 0) { + return String.format("%d:%02d:%02d/%d:%02d:%02d", + elapsedHours, elapsedMinutes, elapsedSeconds, + durationHours, durationMinutes, durationSeconds); + } else { + return String.format("%02d:%02d/%02d:%02d", + elapsedMinutes, elapsedSeconds, durationMinutes, + durationSeconds); + } + } else { + if (elapsedHours > 0) { + return String.format("%d:%02d:%02d", elapsedHours, + elapsedMinutes, elapsedSeconds); + } else { + return String.format("%02d:%02d", elapsedMinutes, + elapsedSeconds); + } + } + } + + @Override + public final void setFitWidth(double d) { + mediaView.setFitWidth(d); + } + + @Override + public final double getFitWidth() { + return mediaView.getFitWidth(); + } + + @Override + public final DoubleProperty fitWidthProperty() { + return mediaView.fitWidthProperty(); + } + + @Override + public final void setFitHeight(double d) { + mediaView.setFitHeight(d); + } + + @Override + public final double getFitHeight() { + return mediaView.getFitHeight(); + } + + @Override + public final DoubleProperty fitHeightProperty() { + return mediaView.fitHeightProperty(); + } + + public void stopVideo() { + mp.stop(); + } + + @Override + public boolean isPreserveRatio() { + return mediaView.isPreserveRatio(); + } + + @Override + public void setPreserveRatio(boolean b) { + mediaView.setPreserveRatio(b); + } + + @Override + public BooleanProperty preserveRatioProperty() { + return mediaView.preserveRatioProperty(); + } + +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/MetaDataPane.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/MetaDataPane.fxml new file mode 100644 index 0000000000000000000000000000000000000000..195600d74d320e49409dedf8e79a650b88be5a42 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/MetaDataPane.fxml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import java.lang.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.image.*?> +<?import javafx.scene.layout.*?> + +<fx:root id="AnchorPane" type="javafx.scene.layout.AnchorPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> + <children> + <TitledPane animated="false" collapsible="false" expanded="true" text="Details" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"> + <content> + <AnchorPane id="Content"> + <children> + <VBox alignment="TOP_CENTER" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"> + <children> + <BorderPane id="imageAnchor" fx:id="imageBorder" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" VBox.vgrow="NEVER"> + <center> + <ImageView fx:id="imageView" fitHeight="200.0" fitWidth="200.0" pickOnBounds="true" preserveRatio="true" BorderPane.alignment="CENTER" /> + </center> + </BorderPane> + <TableView fx:id="tableView" editable="false" tableMenuButtonVisible="false" VBox.vgrow="ALWAYS"> + <columns> + <TableColumn fx:id="attributeColumn" editable="false" prefWidth="100.0" text="Attribute" /> + <TableColumn fx:id="valueColumn" editable="false" prefWidth="100.0" text="Value" /> + </columns> + </TableView> + </children> + </VBox> + </children> + </AnchorPane> + </content> + </TitledPane> + </children> +</fx:root> diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/MetaDataPane.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/MetaDataPane.java new file mode 100644 index 0000000000000000000000000000000000000000..e47468c42ef65dd3dc2dcd7677eb9d3ba9dc14c5 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/MetaDataPane.java @@ -0,0 +1,260 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.gui; + +import java.io.IOException; +import java.net.URL; +import java.util.Collection; +import java.util.ResourceBundle; +import java.util.logging.Level; +import javafx.application.Platform; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.control.Label; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.Region; +import static javafx.scene.layout.Region.USE_COMPUTED_SIZE; +import javafx.scene.text.Text; +import javafx.util.Callback; +import javafx.util.Pair; +import org.openide.util.Exceptions; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.imageanalyzer.EurekaController; +import org.sleuthkit.autopsy.imageanalyzer.TagUtils; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.Category; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile; +import org.sleuthkit.datamodel.TagName; +import org.sleuthkit.datamodel.TskCoreException; + +/** */ +public class MetaDataPane extends AnchorPane implements Category.CategoryListener, TagUtils.TagListener, DrawableView { + + private static final Logger LOGGER = Logger.getLogger(MetaDataPane.class.getName()); + + private final EurekaController controller; + + private Long fileID; + + @FXML + private ImageView imageView; + + @FXML + private ResourceBundle resources; + + @FXML + private URL location; + + @FXML + private TableColumn<Pair<DrawableAttribute, ? extends Object>, DrawableAttribute> attributeColumn; + + @FXML + private TableView<Pair<DrawableAttribute, ? extends Object>> tableView; + + @FXML + private TableColumn<Pair<DrawableAttribute, ? extends Object>, String> valueColumn; + + @FXML + private BorderPane imageBorder; + + private DrawableFile file; + + @Override + public Long getFileID() { + return fileID; + } + + @FXML + void initialize() { + assert attributeColumn != null : "fx:id=\"attributeColumn\" was not injected: check your FXML file 'MetaDataPane.fxml'."; + assert imageView != null : "fx:id=\"imageView\" was not injected: check your FXML file 'MetaDataPane.fxml'."; + assert tableView != null : "fx:id=\"tableView\" was not injected: check your FXML file 'MetaDataPane.fxml'."; + assert valueColumn != null : "fx:id=\"valueColumn\" was not injected: check your FXML file 'MetaDataPane.fxml'."; + TagUtils.registerListener(this); + Category.registerListener(this); + + tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + tableView.setPlaceholder(new Label("Select a file to show its details here.")); + + attributeColumn.setCellValueFactory((TableColumn.CellDataFeatures<Pair<DrawableAttribute, ?>, DrawableAttribute> param) -> new SimpleObjectProperty<>(param.getValue().getKey())); + attributeColumn.setCellFactory(new Callback<TableColumn<Pair<DrawableAttribute, ? extends Object>, DrawableAttribute>, TableCell<Pair<DrawableAttribute, ? extends Object>, DrawableAttribute>>() { + + @Override + public TableCell<Pair<DrawableAttribute, ? extends Object>, DrawableAttribute> call(TableColumn<Pair<DrawableAttribute, ? extends Object>, DrawableAttribute> param) { + return new TableCell<Pair<DrawableAttribute, ? extends Object>, DrawableAttribute>() { + + @Override + protected void updateItem(DrawableAttribute item, boolean empty) { + super.updateItem(item, empty); //To change body of generated methods, choose Tools | Templates. + if (item != null) { + setText(item.getDisplayName()); + setGraphic(new ImageView(item.getIcon())); + } else { + setGraphic(null); + setText(null); + } + } + }; + } + }); + + attributeColumn.setPrefWidth(USE_COMPUTED_SIZE); + + valueColumn.setCellValueFactory((TableColumn.CellDataFeatures<Pair<DrawableAttribute, ?>, String> p) -> { + if (p.getValue().getKey() == DrawableAttribute.TAGS) { + return new SimpleStringProperty(TagUtils.collectionToString((Collection<TagName>) p.getValue().getValue())); + } else { + return new SimpleStringProperty(p.getValue().getValue().toString()); + } + }); + valueColumn.setPrefWidth(USE_COMPUTED_SIZE); + valueColumn.setCellFactory(new Callback<TableColumn<Pair<DrawableAttribute, ? extends Object>, String>, TableCell<Pair<DrawableAttribute, ? extends Object>, String>>() { + @Override + public TableCell<Pair<DrawableAttribute, ? extends Object>, String> call(TableColumn<Pair<DrawableAttribute, ? extends Object>, String> p) { + return new TableCell<Pair<DrawableAttribute, ? extends Object>, String>() { + + @Override + public void updateItem(String item, boolean empty) { + super.updateItem(item, empty); + + if (!isEmpty()) { + Text text = new Text(item); + text.wrappingWidthProperty().bind(getTableColumn().widthProperty()); + setGraphic(text); + } else { + setGraphic(null); + } + } + }; + } + }); + + tableView.getColumns().setAll(attributeColumn, valueColumn); + + //listen for selection change + controller.getSelectionModel().lastSelectedProperty().addListener((observable, oldFileID, newFileID) -> { + setFile(newFileID); + }); + +// MetaDataPane.this.visibleProperty().bind(controller.getMetaDataCollapsed().not()); +// MetaDataPane.this.managedProperty().bind(controller.getMetaDataCollapsed().not()); + } + + @Override + public DrawableFile getFile() { + if (fileID != null) { + if (file == null || file.getId() != fileID) { + try { + file = controller.getFileFromId(fileID); + } catch (TskCoreException ex) { + LOGGER.log(Level.WARNING, "failed to get DrawableFile for obj_id" + fileID, ex); + return null; + } + } + } else { + return null; + } + return file; + } + + @Override + public void setFile(Long fileID) { + this.fileID = fileID; + + if (fileID == null) { + + Platform.runLater(() -> { + imageView.setImage(null); + tableView.getItems().clear(); + getBorderable().setBorder(null); + + }); + } else { + try { + file = controller.getFileFromId(fileID); + updateUI(); + + file.categoryProperty().addListener(new ChangeListener<Category>() { + @Override + public void changed(ObservableValue<? extends Category> ov, Category t, final Category t1) { + updateUI(); + } + }); + } catch (TskCoreException ex) { + Exceptions.printStackTrace(ex); + } + } + } + + public MetaDataPane(EurekaController controller) { + this.controller = controller; + + FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("MetaDataPane.fxml")); + fxmlLoader.setRoot(this); + fxmlLoader.setController(this); + + try { + fxmlLoader.load(); + } catch (IOException exception) { + throw new RuntimeException(exception); + } + } + + public void updateUI() { + final Image icon = getFile().getIcon(); + final ObservableList attributesList = getFile().getAttributesList(); + + Platform.runLater(() -> { + imageView.setImage(icon); + tableView.getItems().setAll(attributesList); + }); + + updateCategoryBorder(); + } + + @Override + public Region getBorderable() { + return imageBorder; + } + + @Override + public void handleCategoryChanged(Collection<Long> ids) { + if (getFile() != null && ids.contains(getFileID())) { + updateUI(); + } + } + + @Override + public void handleTagsChanged(Collection<Long> ids) { + if (getFile() != null && ids.contains(getFileID())) { + updateUI(); + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/NoGroupsDialog.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/NoGroupsDialog.fxml new file mode 100644 index 0000000000000000000000000000000000000000..0f1e8beb113d9021b50a5d3a85c3146eb2ab39f0 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/NoGroupsDialog.fxml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import java.lang.*?> +<?import javafx.geometry.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.effect.*?> +<?import javafx.scene.image.*?> +<?import javafx.scene.layout.*?> +<?import javafx.scene.text.*?> + +<fx:root maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" style="-fx-background-color: white;" type="GridPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> + <columnConstraints> + <ColumnConstraints halignment="CENTER" hgrow="NEVER" maxWidth="53.0" minWidth="10.0" prefWidth="53.0" /> + <ColumnConstraints hgrow="NEVER" minWidth="10.0" /> + </columnConstraints> + <rowConstraints> + <RowConstraints minHeight="10.0" vgrow="SOMETIMES" /> + </rowConstraints> + <children> + <BorderPane fx:id="graphicBorder" GridPane.halignment="CENTER" GridPane.valignment="CENTER"> + <center> + <ImageView fitHeight="32.0" fitWidth="32.0" pickOnBounds="true" preserveRatio="true" BorderPane.alignment="CENTER" GridPane.halignment="CENTER" GridPane.rowSpan="2" GridPane.valignment="CENTER"> + <image> + <Image url="@../images/information.png" /> + </image> + <GridPane.margin> + <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" /> + </GridPane.margin> + </ImageView> + </center> + </BorderPane> + <Label fx:id="messageLabel" prefWidth="300.0" text="There are no events visible with the current zoom / filter settings. Adjust the settings or:" wrapText="true" GridPane.columnIndex="1"> + <GridPane.margin> + <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" /> + </GridPane.margin> + <font> + <Font size="14.0" /> + </font> + </Label> + </children> + <effect> + <DropShadow /> + </effect> +<padding> +<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" /> +</padding> +</fx:root> diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/NoGroupsDialog.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/NoGroupsDialog.java new file mode 100644 index 0000000000000000000000000000000000000000..77bd00cc1f4ee940113203d9a7221a8b57ff1e5f --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/NoGroupsDialog.java @@ -0,0 +1,69 @@ + +/* + * Autopsy Forensic Browser + * + * Copyright 2013-14 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.gui; + +import org.sleuthkit.autopsy.imageanalyzer.FXMLConstructor; +import java.net.URL; +import java.util.ResourceBundle; +import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.GridPane; + +/** + * + */ +public class NoGroupsDialog extends GridPane { + + @FXML + private ResourceBundle resources; + + @FXML + private URL location; + + @FXML + private BorderPane graphicBorder; + + @FXML + private Label messageLabel; + + @FXML + void initialize() { + assert graphicBorder != null : "fx:id=\"graphicBorder\" was not injected: check your FXML file 'NoGroupsDialog.fxml'."; + assert messageLabel != null : "fx:id=\"messageLabel\" was not injected: check your FXML file 'NoGroupsDialog.fxml'."; + + } + + private NoGroupsDialog() { + FXMLConstructor.construct(this, "NoGroupsDialog.fxml"); + } + + public NoGroupsDialog(String message) { + this(); + messageLabel.setText(message); + } + + public NoGroupsDialog(String message, Node graphic) { + this(); + messageLabel.setText(message); + graphicBorder.setCenter(graphic); + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SingleDrawableViewBase.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SingleDrawableViewBase.java new file mode 100644 index 0000000000000000000000000000000000000000..eb62925114e53840074575767227c0ac664421c2 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SingleDrawableViewBase.java @@ -0,0 +1,399 @@ + +/* + * Autopsy Forensic Browser + * + * Copyright 2013-14 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.gui; + +import org.sleuthkit.autopsy.imageanalyzer.EurekaController; +import org.sleuthkit.autopsy.imageanalyzer.EurekaTopComponent; +import org.sleuthkit.autopsy.imageanalyzer.FileIDSelectionModel; +import org.sleuthkit.autopsy.imageanalyzer.FileUpdateEvent; +import org.sleuthkit.autopsy.imageanalyzer.TagUtils; +import org.sleuthkit.autopsy.imageanalyzer.ThreadConfined; +import org.sleuthkit.autopsy.imageanalyzer.ThreadConfined.ThreadType; +import org.sleuthkit.autopsy.imageanalyzer.actions.AddDrawableTagAction; +import org.sleuthkit.autopsy.imageanalyzer.actions.CategorizeAction; +import org.sleuthkit.autopsy.imageanalyzer.actions.SwingMenuItemAdapter; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.Category; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.logging.Level; +import javafx.application.Platform; +import javafx.beans.Observable; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.fxml.FXML; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.Label; +import javafx.scene.control.MenuItem; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.Tooltip; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.AnchorPane; +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.Region; +import javafx.scene.paint.Color; +import javax.swing.Action; +import javax.swing.SwingUtilities; +import org.openide.util.Exceptions; +import org.openide.util.Lookup; +import org.openide.util.actions.Presenter; +import org.openide.windows.TopComponent; +import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.corecomponentinterfaces.ContextMenuActionsProvider; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.datamodel.FileNode; +import org.sleuthkit.autopsy.directorytree.DirectoryTreeTopComponent; +import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; +import org.sleuthkit.autopsy.directorytree.ExtractAction; +import org.sleuthkit.autopsy.directorytree.NewWindowViewAction; +import org.sleuthkit.datamodel.ContentTag; +import org.sleuthkit.datamodel.TagName; +import org.sleuthkit.datamodel.TskCoreException; + +/** An abstract base class for {@link DrawableTile} and {@link SlideShowView}, + * since they share a similar node tree and many behaviors, other implementers + * of {@link DrawableView}s should implement the interface directly + * */ +public abstract class SingleDrawableViewBase extends AnchorPane implements DrawableView { + + private static final Logger LOGGER = Logger.getLogger(SingleDrawableViewBase.class.getName()); + + private static final Border UNSELECTED_ORDER = new Border(new BorderStroke(Color.GRAY, BorderStrokeStyle.SOLID, new CornerRadii(2), new BorderWidths(3))); + + private static final Border SELECTED_BORDER = new Border(new BorderStroke(Color.BLUE, BorderStrokeStyle.SOLID, new CornerRadii(2), new BorderWidths(3))); + + //TODO: should this stuff be done in CSS? -jm + protected static final Image videoIcon = new Image("org/sleuthkit/autopsy/imageanalyzer/images/video-file.png"); + + protected static final Image hashHitIcon = new Image("org/sleuthkit/autopsy/imageanalyzer/images/hashset_hits.png"); + + protected static final Image followUpIcon = new Image("org/sleuthkit/autopsy/imageanalyzer/images/flag_red.png"); + + protected static final Image followUpGray = new Image("org/sleuthkit/autopsy/imageanalyzer/images/flag_gray.png"); + + protected static final FileIDSelectionModel globalSelectionModel = FileIDSelectionModel.getInstance(); + + /** + * displays the icon representing video files + */ + @FXML + protected ImageView fileTypeImageView; + + /** + * displays the icon representing hash hits + */ + @FXML + protected ImageView hashHitImageView; + + /** + * displays the icon representing follow up tag + */ + @FXML + protected ImageView followUpImageView; + + @FXML + protected ToggleButton followUpToggle; + + /** + * the label that shows the name of the represented file + */ + @FXML + protected Label nameLabel; + + @FXML + protected BorderPane imageBorder; + + static private ContextMenu contextMenu; + + protected DrawableFile file; + + protected Long fileID; + + /** the groupPane this {@link SingleDrawableViewBase} is embedded in */ + protected GroupPane groupPane; + + protected SingleDrawableViewBase() { + + globalSelectionModel.getSelected().addListener((Observable observable) -> { + updateSelectionState(); + }); + + //set up mouse listener + //TODO: split this between DrawableTile and SingleDrawableViewBase + addEventFilter(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() { + + @Override + public void handle(MouseEvent t) { + + switch (t.getButton()) { + case PRIMARY: + if (t.getClickCount() == 1) { + if (t.isControlDown()) { + globalSelectionModel.toggleSelection(fileID); + } else { + groupPane.makeSelection(t.isShiftDown(), fileID); + } + } else if (t.getClickCount() > 1) { + groupPane.activateSlideShowViewer(fileID); + } + break; + case SECONDARY: + + if (t.getClickCount() == 1) { + if (globalSelectionModel.isSelected(fileID) == false) { + groupPane.makeSelection(false, fileID); + } + } + + if (contextMenu != null) { + contextMenu.hide(); + } + final ContextMenu groupContextMenu = groupPane.getContextMenu(); + if (groupContextMenu != null) { + groupContextMenu.hide(); + } + contextMenu = buildContextMenu(); + contextMenu.show(SingleDrawableViewBase.this, t.getScreenX(), t.getScreenY()); + + break; + } + t.consume(); + } + + private ContextMenu buildContextMenu() { + final ArrayList<MenuItem> menuItems = new ArrayList<>(); + + menuItems.add(CategorizeAction.getPopupMenu()); + + menuItems.add(AddDrawableTagAction.getInstance().getPopupMenu()); + + final MenuItem extractMenuItem = new MenuItem("Extract File(s)"); + extractMenuItem.setOnAction((ActionEvent t) -> { + SwingUtilities.invokeLater(() -> { + TopComponent etc = WindowManager.getDefault().findTopComponent(EurekaTopComponent.PREFERRED_ID); + ExtractAction.getInstance().actionPerformed(new java.awt.event.ActionEvent(etc, 0, null)); + }); + }); + menuItems.add(extractMenuItem); + + MenuItem contentViewer = new MenuItem("Show Content Viewer"); + contentViewer.setOnAction((ActionEvent t) -> { + SwingUtilities.invokeLater(() -> { + new NewWindowViewAction("Show Content Viewer", new FileNode(getFile().getAbstractFile())).actionPerformed(null); + }); + }); + menuItems.add(contentViewer); + + MenuItem externalViewer = new MenuItem("Open in External Viewer"); + final ExternalViewerAction externalViewerAction = new ExternalViewerAction("Open in External Viewer", new FileNode(getFile().getAbstractFile())); + + externalViewer.setDisable(externalViewerAction.isEnabled() == false); + externalViewer.setOnAction((ActionEvent t) -> { + SwingUtilities.invokeLater(() -> { + externalViewerAction.actionPerformed(null); + }); + }); + menuItems.add(externalViewer); + + Collection<? extends ContextMenuActionsProvider> menuProviders = Lookup.getDefault().lookupAll(ContextMenuActionsProvider.class); + + for (ContextMenuActionsProvider provider : menuProviders) { + for (final Action act : provider.getActions()) { + if (act instanceof Presenter.Popup) { + Presenter.Popup aact = (Presenter.Popup) act; + menuItems.add(SwingMenuItemAdapter.create(aact.getPopupPresenter())); + } + } + } + + ContextMenu contextMenu = new ContextMenu(menuItems.toArray(new MenuItem[]{})); + contextMenu.setAutoHide(true); + return contextMenu; + } + }); + } + + @ThreadConfined(type = ThreadType.UI) + protected abstract void clearContent(); + + protected abstract void disposeContent(); + + protected abstract Runnable getContentUpdateRunnable(); + + protected abstract String getLabelText(); + + protected void initialize() { + followUpToggle.setOnAction((ActionEvent t) -> { + if (followUpToggle.isSelected() == true) { + globalSelectionModel.clearAndSelect(fileID); + try { + AddDrawableTagAction.getInstance().addTag(TagUtils.getFollowUpTagName(), ""); + } catch (TskCoreException ex) { + Exceptions.printStackTrace(ex); + } + } else { + + //TODO: convert this to an action! + List<ContentTag> contentTagsByContent; + try { + contentTagsByContent = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByContent(getFile()); + for (ContentTag ct : contentTagsByContent) { + if (ct.getName().getDisplayName().equals(TagUtils.getFollowUpTagName().getDisplayName())) { + Case.getCurrentCase().getServices().getTagsManager().deleteContentTag(ct); + SwingUtilities.invokeLater(() -> DirectoryTreeTopComponent.findInstance().refreshContentTreeSafe()); + } + } + EurekaController.getDefault().handleFileUpdate(new FileUpdateEvent(Collections.singleton(fileID), DrawableAttribute.TAGS)); + } catch (TskCoreException ex) { + Exceptions.printStackTrace(ex); + } + } + }); + } + + @Override + public DrawableFile getFile() { + if (fileID != null) { + if (file == null || file.getId() != fileID) { + try { + file = EurekaController.getDefault().getFileFromId(fileID); + } catch (TskCoreException ex) { + LOGGER.log(Level.WARNING, "failed to get DrawableFile for obj_id" + fileID, ex); + return null; + } + } + } else { + return null; + } + return file; + } + + protected boolean hasFollowUp() throws TskCoreException { + String followUpTagName = TagUtils.getFollowUpTagName().getDisplayName(); + Collection<TagName> tagNames = (Collection<TagName>) getFile().getValueOfAttribute(DrawableAttribute.TAGS); + return tagNames.stream().anyMatch((tn) -> tn.getDisplayName().equals(followUpTagName)); + } + + protected void updateUI(final boolean isVideo, final boolean hasHashSetHits, final String text) { + if (isVideo) { + fileTypeImageView.setImage(videoIcon); + } else { + fileTypeImageView.setImage(null); + } + + if (hasHashSetHits) { + hashHitImageView.setImage(hashHitIcon); + } else { + hashHitImageView.setImage(null); + } + + nameLabel.setText(text); + nameLabel.setTooltip(new Tooltip(text)); + } + + @Override + public Long getFileID() { + return fileID; + } + + @Override + public void handleTagsChanged(Collection<Long> ids) { + if (fileID != null && ids.contains(fileID)) { + updateFollowUpIcon(); + } + } + + protected void updateFollowUpIcon() { + if (file != null) { + try { + boolean hasFollowUp = hasFollowUp(); + Platform.runLater(() -> { + followUpImageView.setImage(hasFollowUp ? followUpIcon : followUpGray); + followUpToggle.setSelected(hasFollowUp); + }); + } catch (TskCoreException ex) { + Exceptions.printStackTrace(ex); + } + } + } + + @Override + public void setFile(final Long fileID) { + if (Objects.equals(fileID, this.fileID) == false) { + this.fileID = fileID; + disposeContent(); + if (this.fileID == null) { + Category.unregisterListener(this); + TagUtils.unregisterListener(this); + file = null; + Platform.runLater(() -> { + clearContent(); + }); + } else { + Category.registerListener(this); + TagUtils.registerListener(this); + + getFile(); + updateSelectionState(); + updateCategoryBorder(); + updateFollowUpIcon(); + final String text = getLabelText(); + final boolean isVideo = file.isVideo(); + final boolean hasHashSetHits = hasHashHit(); + Platform.runLater(() -> { + updateUI(isVideo, hasHashSetHits, text); + }); + + Platform.runLater(getContentUpdateRunnable()); + } + } + } + + private void updateSelectionState() { + final boolean selected = globalSelectionModel.isSelected(fileID); + Platform.runLater(() -> { + SingleDrawableViewBase.this.setBorder(selected ? SELECTED_BORDER : UNSELECTED_ORDER); + }); + } + + @Override + public Region getBorderable() { + return imageBorder; + } + + @Override + public void handleCategoryChanged(Collection<Long> ids) { + if (ids.contains(fileID)) { + updateCategoryBorder(); + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SlideShow.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SlideShow.fxml new file mode 100644 index 0000000000000000000000000000000000000000..2f2c783472d6ef125b8222b7f1d67220772372fd --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SlideShow.fxml @@ -0,0 +1,151 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import java.lang.*?> +<?import java.net.*?> +<?import javafx.geometry.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.image.*?> +<?import javafx.scene.layout.*?> +<?import org.controlsfx.control.*?> +<?scenebuilder-stylesheet GroupPane.css?> + +<fx:root type="AnchorPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> + <children> + <HBox AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"> + <children> + <HBox maxWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" BorderPane.alignment="CENTER"> + <children> + <Button fx:id="leftButton" contentDisplay="GRAPHIC_ONLY" maxHeight="1.7976931348623157E308" mnemonicParsing="false" text="" HBox.hgrow="NEVER"> + <graphic> + <ImageView fitHeight="128.0" fitWidth="32.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="false" rotate="180.0" scaleX="1.0" translateX="0.0"> + <image> + <Image url="@../images/right_arrow_128.png" /> + </image> + </ImageView> + </graphic> + </Button> + </children> + </HBox> + <BorderPane fx:id="internalBorderPane" maxWidth="-1.0" prefHeight="-1.0" prefWidth="-1.0" BorderPane.alignment="CENTER" HBox.hgrow="ALWAYS"> + <top> + <ToolBar fx:id="toolBar" maxHeight="-Infinity"> + <items> + <Region fx:id="spring" prefHeight="-1.0" prefWidth="-1.0" /> + <Label text="Apply to File:" /> + <SplitMenuButton id="tagSplitMenu" fx:id="tagSplitButton" disable="false" mnemonicParsing="false" text="Follow Up" textOverrun="ELLIPSIS"> + <items> + <MenuItem mnemonicParsing="false" text="Action 1" /> + <MenuItem mnemonicParsing="false" text="Action 2" /> + </items> + </SplitMenuButton> + <Region prefHeight="-1.0" prefWidth="10.0" /> + <Label text="Category:"> + <graphic> + <ImageView fitHeight="16.0" fitWidth="16.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true"> + <image> + <Image url="@../images/category-icon.png" /> + </image> + </ImageView> + </graphic> + </Label> + <HBox id="HBox" alignment="CENTER" spacing="0.0" styleClass="segmented-button-bar"> + <children> + + <Region prefHeight="-1.0" prefWidth="5.0" /> + <org.controlsfx.control.SegmentedButton> + <buttons> + <ToggleButton fx:id="cat0Toggle" mnemonicParsing="false" text="0" HBox.hgrow="ALWAYS" /> + <ToggleButton fx:id="cat1Toggle" mnemonicParsing="false" style="" styleClass="button" text="1" HBox.hgrow="ALWAYS" /> + <ToggleButton id="Cat2Toggle" fx:id="cat2Toggle" mnemonicParsing="false" styleClass="button" text="2" HBox.hgrow="ALWAYS" /> + <ToggleButton fx:id="cat3Toggle" mnemonicParsing="false" styleClass="button" text="3" HBox.hgrow="ALWAYS" /> + <ToggleButton fx:id="cat4Toggle" mnemonicParsing="false" styleClass="button" text="4" HBox.hgrow="ALWAYS" /> + <ToggleButton fx:id="cat5Toggle" mnemonicParsing="false" text="5" toggleGroup="$cat" HBox.hgrow="ALWAYS" /> + </buttons> + + </org.controlsfx.control.SegmentedButton> + <Region prefHeight="-1.0" prefWidth="5.0" /> + + </children> + <stylesheets> + <URL value="@GroupPane.css" /> + </stylesheets> + </HBox> + </items> + </ToolBar> + </top> + <center> + <AnchorPane maxHeight="-1.0" maxWidth="-1.0" minHeight="-Infinity" minWidth="-Infinity" opacity="1.0" prefHeight="-1.0" prefWidth="-1.0" style="-fx-border-width: 1; -fx-border-color: darkgray; -fx-border-radius: 2; -fx-background-color: linear-gradient(to bottom, derive(-fx-base,-30%), derive(-fx-base,-60%)), linear-gradient(to bottom, derive(-fx-base,65%) 2%, derive(-fx-base,-20%) 95%); -fx-background-radius: 2;" BorderPane.alignment="CENTER"> + <children> + <BorderPane maxHeight="-1.0" maxWidth="-1.0" minHeight="-Infinity" minWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" snapToPixel="true" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"> + <bottom> + <BorderPane fx:id="footer" maxHeight="-Infinity" maxWidth="-1.0" minHeight="-Infinity" minWidth="-1.0" prefHeight="-1.0" prefWidth="-1.0" BorderPane.alignment="CENTER"> + <center> + <Label id="pathLabel" fx:id="nameLabel" alignment="CENTER" contentDisplay="TEXT_ONLY" maxHeight="16.0" minHeight="16.0" minWidth="-1.0" prefHeight="-1.0" prefWidth="-1.0" text="file name" textAlignment="CENTER" wrapText="true"> + <labelFor> + <ImageView fitHeight="200.0" fitWidth="200.0" opacity="1.0" pickOnBounds="true" preserveRatio="true" style="-fx-border-radius : 5; -fx-border-width : 5; -fx-border-color : blue;" BorderPane.alignment="CENTER" /> + </labelFor> + </Label> + </center> + <left> + <HBox maxHeight="-Infinity" prefHeight="-1.0" prefWidth="-1.0" spacing="2.0" BorderPane.alignment="CENTER_LEFT"> + <children> + <ImageView fx:id="fileTypeImageView" fitHeight="16.0" fitWidth="16.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true" scaleX="1.0" scaleY="1.0"> + <image> + <Image url="@../images/video-file.png" /> + </image> + </ImageView> + <ImageView fx:id="hashHitImageView" fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" style=""> + <image> + <Image url="@../images/hashset_hits.png" /> + </image> + <HBox.margin> + <Insets bottom="1.0" left="1.0" right="1.0" top="1.0" /> + </HBox.margin> + </ImageView> + </children> + <padding> + <Insets bottom="2.0" right="2.0" top="2.0" /> + </padding> + </HBox> + </left> + <right> + <ToggleButton fx:id="followUpToggle" minWidth="24.0" mnemonicParsing="false" prefWidth="24.0" selected="false" text=""> + <graphic> + <ImageView id="followUpImageview" fx:id="followUpImageView" fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true"> + <image> + <Image url="@../images/flag_gray.png" /> + </image> + </ImageView> + </graphic> + </ToggleButton> + </right> + </BorderPane> + </bottom> + <center> + <BorderPane fx:id="imageBorder" center="$imageView" maxHeight="-1.0" prefHeight="-1.0" prefWidth="-1.0" style="-fx-border-color: lightgray; -fx-border-width:10; -fx-border-radius:2;" BorderPane.alignment="CENTER" /> + </center> + </BorderPane> + </children> + <padding> + <Insets bottom="2.0" left="2.0" right="2.0" top="2.0" /> + </padding> + </AnchorPane> + </center> + </BorderPane> + <HBox maxWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" BorderPane.alignment="CENTER"> + <children> + <Button fx:id="rightButton" contentDisplay="GRAPHIC_ONLY" graphicTextGap="0.0" maxHeight="1.7976931348623157E308" mnemonicParsing="false" prefHeight="-1.0" prefWidth="-1.0" text="" HBox.hgrow="NEVER"> + <graphic> + <ImageView fitHeight="128.0" fitWidth="32.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="false" scaleX="1.0" smooth="true" translateX="0.0"> + <image> + <Image url="@../images/right_arrow_128.png" /> + </image> + </ImageView> + </graphic> + </Button> + </children> + </HBox> + </children> + </HBox> + </children> +</fx:root> diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SlideShowView.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SlideShowView.java new file mode 100644 index 0000000000000000000000000000000000000000..635737a0d8e0b0a5fb652d36c141666100c9a25c --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SlideShowView.java @@ -0,0 +1,363 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.gui; + +import java.util.ArrayList; +import javafx.application.Platform; +import javafx.beans.Observable; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.geometry.HorizontalDirection; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.MenuItem; +import javafx.scene.control.SplitMenuButton; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.ToolBar; +import javafx.scene.image.ImageView; +import static javafx.scene.input.KeyCode.LEFT; +import static javafx.scene.input.KeyCode.RIGHT; +import javafx.scene.input.KeyEvent; +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.Priority; +import javafx.scene.layout.Region; +import org.openide.util.Exceptions; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.imageanalyzer.FXMLConstructor; +import org.sleuthkit.autopsy.imageanalyzer.FileIDSelectionModel; +import org.sleuthkit.autopsy.imageanalyzer.TagUtils; +import org.sleuthkit.autopsy.imageanalyzer.ThreadConfined; +import org.sleuthkit.autopsy.imageanalyzer.ThreadConfined.ThreadType; +import org.sleuthkit.autopsy.imageanalyzer.ThreadUtils; +import org.sleuthkit.autopsy.imageanalyzer.actions.CategorizeAction; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.Category; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute; +import org.sleuthkit.datamodel.TagName; +import org.sleuthkit.datamodel.TskCoreException; + +/** Displays the files of a group one at a time. Designed to be embedded in a + * GroupPane. + * TODO: Extract a subclass for video files in slideshow mode-jm + * TODO: reduce coupling to GroupPane */ +public class SlideShowView extends SingleDrawableViewBase implements TagUtils.TagListener, Category.CategoryListener { + + private static final Logger LOGGER = Logger.getLogger(SlideShowView.class.getName()); + + @FXML + private ToggleButton cat0Toggle; + + @FXML + private ToggleButton cat2Toggle; + + @FXML + private SplitMenuButton tagSplitButton; + + @FXML + private ToggleButton cat3Toggle; + + @FXML + private Region spring; + + @FXML + private Button leftButton; + + @FXML + private ToggleButton cat4Toggle; + + @FXML + private ToggleButton cat5Toggle; + + @FXML + private ToggleButton cat1Toggle; + + @FXML + private Button rightButton; + + @FXML + private ToolBar toolBar; + + @FXML + private BorderPane footer; + + @FXML + @Override + protected void initialize() { + super.initialize(); + assert cat0Toggle != null : "fx:id=\"cat0Toggle\" was not injected: check your FXML file 'SlideShow.fxml'."; + assert cat1Toggle != null : "fx:id=\"cat1Toggle\" was not injected: check your FXML file 'SlideShow.fxml'."; + assert cat2Toggle != null : "fx:id=\"cat2Toggle\" was not injected: check your FXML file 'SlideShow.fxml'."; + assert cat3Toggle != null : "fx:id=\"cat3Toggle\" was not injected: check your FXML file 'SlideShow.fxml'."; + assert cat4Toggle != null : "fx:id=\"cat4Toggle\" was not injected: check your FXML file 'SlideShow.fxml'."; + assert cat5Toggle != null : "fx:id=\"cat5Toggle\" was not injected: check your FXML file 'SlideShow.fxml'."; + assert leftButton != null : "fx:id=\"leftButton\" was not injected: check your FXML file 'SlideShow.fxml'."; + assert rightButton != null : "fx:id=\"rightButton\" was not injected: check your FXML file 'SlideShow.fxml'."; + assert tagSplitButton != null : "fx:id=\"tagSplitButton\" was not injected: check your FXML file 'SlideShow.fxml'."; + + Platform.runLater(() -> { + HBox.setHgrow(spring, Priority.ALWAYS); + spring.setMinWidth(Region.USE_PREF_SIZE); + }); + + tagSplitButton.setOnAction((ActionEvent t) -> { + try { + TagUtils.createSelTagMenuItem(TagUtils.getFollowUpTagName(), tagSplitButton).getOnAction().handle(t); + } catch (TskCoreException ex) { + Exceptions.printStackTrace(ex); + } + }); + + tagSplitButton.setGraphic(new ImageView(DrawableAttribute.TAGS.getIcon())); + tagSplitButton.showingProperty().addListener((ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) -> { + if (t1) { + ArrayList<MenuItem> selTagMenues = new ArrayList<>(); + for (final TagName tn : TagUtils.getNonCategoryTagNames()) { + MenuItem menuItem = TagUtils.createSelTagMenuItem(tn, tagSplitButton); + selTagMenues.add(menuItem); + } + tagSplitButton.getItems().setAll(selTagMenues); + } + }); + cat0Toggle.setBorder(new Border(new BorderStroke(Category.ZERO.getColor(), BorderStrokeStyle.SOLID, new CornerRadii(1), new BorderWidths(1)))); + cat1Toggle.setBorder(new Border(new BorderStroke(Category.ONE.getColor(), BorderStrokeStyle.SOLID, new CornerRadii(1), new BorderWidths(1)))); + cat2Toggle.setBorder(new Border(new BorderStroke(Category.TWO.getColor(), BorderStrokeStyle.SOLID, new CornerRadii(1), new BorderWidths(1)))); + cat3Toggle.setBorder(new Border(new BorderStroke(Category.THREE.getColor(), BorderStrokeStyle.SOLID, new CornerRadii(1), new BorderWidths(1)))); + cat4Toggle.setBorder(new Border(new BorderStroke(Category.FOUR.getColor(), BorderStrokeStyle.SOLID, new CornerRadii(1), new BorderWidths(1)))); + cat5Toggle.setBorder(new Border(new BorderStroke(Category.FIVE.getColor(), BorderStrokeStyle.SOLID, new CornerRadii(1), new BorderWidths(1)))); + + cat0Toggle.selectedProperty().addListener(new CategorizeToggleHandler(Category.ZERO)); + cat1Toggle.selectedProperty().addListener(new CategorizeToggleHandler(Category.ONE)); + cat2Toggle.selectedProperty().addListener(new CategorizeToggleHandler(Category.TWO)); + cat3Toggle.selectedProperty().addListener(new CategorizeToggleHandler(Category.THREE)); + cat4Toggle.selectedProperty().addListener(new CategorizeToggleHandler(Category.FOUR)); + cat5Toggle.selectedProperty().addListener(new CategorizeToggleHandler(Category.FIVE)); + + cat0Toggle.toggleGroupProperty().addListener((o, oldGroup, newGroup) -> { + newGroup.selectedToggleProperty().addListener((ov, oldToggle, newToggle) -> { + if (newToggle == null) { + oldToggle.setSelected(true); + } + }); + }); + + leftButton.setOnAction((ActionEvent t) -> { + cycleSlideShowImage(HorizontalDirection.LEFT); + }); + rightButton.setOnAction((ActionEvent t) -> { + cycleSlideShowImage(HorizontalDirection.RIGHT); + }); + + //set up key listener equivalents of buttons + addEventFilter(KeyEvent.KEY_PRESSED, (KeyEvent t) -> { + + if (t.getEventType() == KeyEvent.KEY_PRESSED) { + switch (t.getCode()) { + case LEFT: + cycleSlideShowImage(HorizontalDirection.LEFT); + break; + case RIGHT: + cycleSlideShowImage(HorizontalDirection.RIGHT); + break; + + //TODO: find a way to share these with grouppane/tileview... (ActionMap?) + case NUMPAD0: + case DIGIT0: + new CategorizeAction().addTag(Category.ZERO.getTagName(), ""); + break; + case NUMPAD1: + case DIGIT1: + new CategorizeAction().addTag(Category.ONE.getTagName(), ""); + break; + case NUMPAD2: + case DIGIT2: + new CategorizeAction().addTag(Category.TWO.getTagName(), ""); + break; + case NUMPAD3: + case DIGIT3: + new CategorizeAction().addTag(Category.THREE.getTagName(), ""); + break; + case NUMPAD4: + case DIGIT4: + new CategorizeAction().addTag(Category.FOUR.getTagName(), ""); + break; + case NUMPAD5: + case DIGIT5: + new CategorizeAction().addTag(Category.FIVE.getTagName(), ""); + break; + } + t.consume(); + } + }); + + syncButtonVisibility(); + + groupPane.grouping() + .addListener((Observable observable) -> { + syncButtonVisibility(); + groupPane.getGrouping().fileIds().addListener((Observable observable1) -> { + syncButtonVisibility(); + }); + } + ); + } + + @ThreadConfined(type = ThreadType.ANY) + private void syncButtonVisibility() { + final boolean hasMultipleFiles = groupPane.getGrouping().fileIds().size() > 1; + Platform.runLater(() -> { + rightButton.setVisible(hasMultipleFiles); + leftButton.setVisible(hasMultipleFiles); + rightButton.setManaged(hasMultipleFiles); + leftButton.setManaged(hasMultipleFiles); + }); + } + + SlideShowView(GroupPane gp) { + super(); + groupPane = gp; + FXMLConstructor.construct(this, "SlideShow.fxml"); + + } + + @ThreadConfined(type = ThreadType.UI) + public void stopVideo() { + if (imageBorder.getCenter() instanceof MediaControl) { + ((MediaControl) imageBorder.getCenter()).stopVideo(); + } + } + + @Override + public void setFile(final Long fileID) { + super.setFile(fileID); + if (this.fileID != null) { + groupPane.makeSelection(false, this.fileID); + } + } + + @Override + protected void disposeContent() { + stopVideo(); + } + + @Override + @ThreadConfined(type = ThreadType.UI) + protected void clearContent() { + imageBorder.setCenter(null); + } + + @Override + protected Runnable getContentUpdateRunnable() { + Node newCenterNode = file.getFullsizeDisplayNode(); + if (newCenterNode instanceof Fitable) { + Fitable fitable = (Fitable) newCenterNode; + fitable.setPreserveRatio(true); + //JMTODO: this math is hack! fix it -jm + fitable.fitWidthProperty().bind(imageBorder.widthProperty().subtract(CAT_BORDER_WIDTH * 2)); + fitable.fitHeightProperty().bind(this.heightProperty().subtract(CAT_BORDER_WIDTH * 4).subtract(footer.heightProperty()).subtract(toolBar.heightProperty())); + } + return () -> { + imageBorder.setCenter(newCenterNode); + }; + } + + @Override + protected String getLabelText() { + return file.getName() + " " + getSupplementalText(); + } + + private void cycleSlideShowImage(HorizontalDirection d) { + + stopVideo(); + if (fileID != null) { + int index = groupPane.getGrouping().fileIds().indexOf(fileID); + final int size = groupPane.getGrouping().fileIds().size(); + index = (index + ((d == HorizontalDirection.LEFT) ? -1 : 1)) % size; + if (index < 0) { + index += size; + } + setFile(groupPane.getGrouping().fileIds().get(index)); + + } else { + setFile(groupPane.getGrouping().fileIds().get(0)); + } + + } + + /** @return supplemental text to include in the label, specifically: + * "image x of y" */ + private String getSupplementalText() { + return " ( " + (groupPane.getGrouping().fileIds().indexOf(fileID) + 1) + " of " + groupPane.getGrouping().fileIds().size() + " in group )"; + } + + @Override + @ThreadConfined(type = ThreadType.ANY) + public Category updateCategoryBorder() { + final Category category = super.updateCategoryBorder(); + ToggleButton toggleForCategory = getToggleForCategory(category); + + ThreadUtils.runNowOrLater(() -> { + toggleForCategory.setSelected(true); + }); + return category; + } + + private ToggleButton getToggleForCategory(Category category) { + switch (category) { + case ZERO: + return cat0Toggle; + case ONE: + return cat1Toggle; + case TWO: + return cat2Toggle; + case THREE: + return cat3Toggle; + case FOUR: + return cat4Toggle; + case FIVE: + return cat5Toggle; + default: + throw new IllegalArgumentException(category.name()); + } + } + + private class CategorizeToggleHandler implements ChangeListener<Boolean> { + + private final Category cat; + + public CategorizeToggleHandler(Category cat) { + this.cat = cat; + } + + @Override + public void changed(ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) { + + if (t1) { + FileIDSelectionModel.getInstance().clearAndSelect(fileID); + new CategorizeAction().addTag(cat.getTagName(), ""); + } + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SortByListCell.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SortByListCell.java new file mode 100644 index 0000000000000000000000000000000000000000..5913fbae22b1f798781eb8aabc49e519c27d204d --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SortByListCell.java @@ -0,0 +1,38 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.gui; + +import javafx.scene.control.ListCell; +import javafx.scene.image.ImageView; +import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupSortBy; + +public class SortByListCell extends ListCell<GroupSortBy> { + + @Override + protected void updateItem(GroupSortBy t, boolean bln) { + super.updateItem(t, bln); + if (t != null) { + setText(t.getDisplayName()); + setGraphic(new ImageView(t.getIcon())); + } else { + setText(null); + setGraphic(null); + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/StatusBar.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/StatusBar.fxml new file mode 100644 index 0000000000000000000000000000000000000000..5f8a0615852e8f21b95d76129047143776a3b5c5 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/StatusBar.fxml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import java.lang.*?> +<?import javafx.geometry.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.image.*?> +<?import javafx.scene.layout.*?> + +<fx:root id="AnchorPane" maxHeight="-Infinity" maxWidth="1.7976931348623157E308" minHeight="-Infinity" minWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" type="javafx.scene.layout.AnchorPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> + <children> + <BorderPane minHeight="-Infinity" minWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"> + <right> + <HBox alignment="CENTER_RIGHT" prefHeight="-1.0" prefWidth="-1.0" BorderPane.alignment="CENTER_RIGHT"> + <children> + <StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" HBox.hgrow="NEVER"> + <children> + <ProgressBar id="progBar" fx:id="fileTaskProgresBar" focusTraversable="false" maxHeight="-1.0" maxWidth="1.7976931348623157E308" minHeight="-Infinity" minWidth="-1.0" prefHeight="24.0" prefWidth="-1.0" progress="0.0" visible="true" /> + <Label id="fileUpdateLabel" fx:id="fileUpdateTaskLabel" alignment="CENTER" contentDisplay="CENTER" graphicTextGap="0.0" labelFor="$fileTaskProgresBar" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefWidth="-1.0" text="0 File Update Tasks" StackPane.alignment="CENTER" /> + </children> + </StackPane> + <StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" HBox.hgrow="NEVER"> + <children> + <ProgressBar fx:id="bgTaskProgressBar" maxHeight="-1.0" maxWidth="-1.0" minHeight="-Infinity" minWidth="-1.0" prefHeight="24.0" prefWidth="-1.0" progress="0.0" StackPane.alignment="CENTER" /> + <Label fx:id="bgTaskLabel" alignment="CENTER" cache="false" contentDisplay="CENTER" disable="false" focusTraversable="false" labelFor="$uiTaskProgressBar" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" text="0 BG Tasks" StackPane.alignment="CENTER" /> + </children> +<HBox.margin> +<Insets right="5.0" /> +</HBox.margin> + </StackPane> + </children> +<BorderPane.margin> +<Insets left="10.0" /> +</BorderPane.margin> + </HBox> + </right> +<center> +<HBox> +<children> + <Label fx:id="statusLabel" maxWidth="-Infinity" minWidth="-Infinity" wrapText="true" BorderPane.alignment="CENTER" HBox.hgrow="ALWAYS"> +<BorderPane.margin> +<Insets left="10.0" right="10.0" /> +</BorderPane.margin> +<HBox.margin> +<Insets left="10.0" right="10.0" /> +</HBox.margin></Label> +</children> +</HBox> +</center> +<left><Label fx:id="staleLabel" text="Some data may be out of date. Enable listening to ingest to update." BorderPane.alignment="CENTER"> +<graphic><ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true"> +<image> +<Image url="@../images/information.png" /> +</image></ImageView> +</graphic> +<BorderPane.margin> +<Insets bottom="5.0" left="5.0" right="10.0" top="5.0" /> +</BorderPane.margin></Label> +</left> + </BorderPane> + </children> +</fx:root> diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/StatusBar.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/StatusBar.java new file mode 100644 index 0000000000000000000000000000000000000000..e3452e12dd9264a8c52289917ce283e94cdf3156 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/StatusBar.java @@ -0,0 +1,113 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-14 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.gui; + +import java.io.IOException; +import java.net.URL; +import java.util.ResourceBundle; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.control.Label; +import javafx.scene.control.ProgressBar; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.AnchorPane; +import org.sleuthkit.autopsy.imageanalyzer.EurekaController; + +/** + * + */ +public class StatusBar extends AnchorPane { + + private final EurekaController controller; + + @FXML + private ResourceBundle resources; + + @FXML + private URL location; + + @FXML + private ProgressBar fileTaskProgresBar; + + @FXML + private Label fileUpdateTaskLabel; + + @FXML + private Label statusLabel; + + @FXML + private Label bgTaskLabel; + + @FXML + private Label staleLabel; + + @FXML + private ProgressBar bgTaskProgressBar; + + @FXML + void initialize() { + assert fileTaskProgresBar != null : "fx:id=\"fileTaskProgresBar\" was not injected: check your FXML file 'StatusBar.fxml'."; + assert fileUpdateTaskLabel != null : "fx:id=\"fileUpdateTaskLabel\" was not injected: check your FXML file 'StatusBar.fxml'."; + assert statusLabel != null : "fx:id=\"statusLabel\" was not injected: check your FXML file 'StatusBar.fxml'."; + assert bgTaskLabel != null : "fx:id=\"uiTaskLabel\" was not injected: check your FXML file 'StatusBar.fxml'."; + assert bgTaskProgressBar != null : "fx:id=\"uiTaskProgressBar\" was not injected: check your FXML file 'StatusBar.fxml'."; + + controller.getFileUpdateQueueSizeProperty().addListener((ov, oldSize, newSize) -> { + Platform.runLater(() -> { + fileUpdateTaskLabel.setText(newSize.toString() + " File Update Tasks"); + fileTaskProgresBar.setProgress((double) (newSize.intValue() > 0 ? -1 : 0)); + }); + }); + + controller.bgTaskQueueSizeProperty().addListener((ov, oldSize, newSize) -> { + Platform.runLater(() -> { + bgTaskLabel.setText(newSize.toString() + " BG Tasks"); + bgTaskProgressBar.setProgress((double) (newSize.intValue() > 0 ? -1 : 0)); + }); + }); + + staleLabel.setTooltip(new Tooltip("Some data may be out of date. Enable listening to ingest in Tools | Options | Image /Video Analyzer , after ingest is complete to update.")); + staleLabel.visibleProperty().bind(controller.stale()); + } + + StatusBar(EurekaController controller) { + this.controller = controller; + FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("StatusBar.fxml")); + fxmlLoader.setRoot(this); + fxmlLoader.setController(this); + + try { + fxmlLoader.load(); + } catch (IOException exception) { + throw new RuntimeException(exception); + } + + } + + public void setLabelText(final String newText) { + Platform.runLater(() -> { + statusLabel.setText(newText); + }); + } + + public String getLabeltext() { + return statusLabel.getText(); + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SummaryTablePane.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SummaryTablePane.fxml new file mode 100644 index 0000000000000000000000000000000000000000..28f59bf9087d3f9d4f8f2dd73003d0153306973a --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SummaryTablePane.fxml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import java.lang.*?> +<?import java.util.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.layout.*?> +<?import javafx.scene.paint.*?> + +<fx:root type="javafx.scene.layout.AnchorPane" id="AnchorPane" maxHeight="-Infinity" maxWidth="-1.0" minHeight="-Infinity" minWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2"> + <children> + <TableView id="summaryTable" fx:id="tableView" prefHeight="-1.0" prefWidth="-1.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"> + <columns> + <TableColumn prefWidth="75.0" text="Category" fx:id="catColumn" /> + <TableColumn prefWidth="75.0" text="# Files" fx:id="countColumn" /> + </columns> + </TableView> + </children> +</fx:root> diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SummaryTablePane.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SummaryTablePane.java new file mode 100644 index 0000000000000000000000000000000000000000..81c034151cb861610b365e636d9fb257dfdc4ebe --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/SummaryTablePane.java @@ -0,0 +1,117 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.gui; + +import org.sleuthkit.autopsy.imageanalyzer.EurekaController; +import org.sleuthkit.autopsy.imageanalyzer.FXMLConstructor; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.Category; +import java.net.URL; +import java.util.Collection; +import java.util.ResourceBundle; +import javafx.application.Platform; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.Priority; +import static javafx.scene.layout.Region.USE_COMPUTED_SIZE; +import javafx.scene.layout.VBox; +import javafx.util.Pair; +import org.openide.util.Exceptions; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.datamodel.TskCoreException; + +/** Displays summary statistics (counts) for each group */ +public class SummaryTablePane extends AnchorPane implements Category.CategoryListener { + + private static SummaryTablePane instance; + + @FXML + private ResourceBundle resources; + + @FXML + private URL location; + + @FXML + private TableColumn<Pair<Category, Integer>, String> catColumn; + + @FXML + private TableColumn<Pair<Category, Integer>, Integer> countColumn; + + @FXML + private TableView<Pair<Category, Integer>> tableView; + + @FXML + void initialize() { + assert catColumn != null : "fx:id=\"catColumn\" was not injected: check your FXML file 'SummaryTablePane.fxml'."; + assert countColumn != null : "fx:id=\"countColumn\" was not injected: check your FXML file 'SummaryTablePane.fxml'."; + assert tableView != null : "fx:id=\"tableView\" was not injected: check your FXML file 'SummaryTablePane.fxml'."; + + //set some properties related to layout/resizing + VBox.setVgrow(this, Priority.NEVER); + tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + tableView.prefHeightProperty().set(7 * 25); + + //set up columns + catColumn.setCellValueFactory((TableColumn.CellDataFeatures<Pair<Category, Integer>, String> p) -> new SimpleObjectProperty<>(p.getValue().getKey().getDisplayName())); + catColumn.setPrefWidth(USE_COMPUTED_SIZE); + + countColumn.setCellValueFactory((TableColumn.CellDataFeatures<Pair<Category, Integer>, Integer> p) -> new SimpleObjectProperty<>(p.getValue().getValue())); + countColumn.setPrefWidth(USE_COMPUTED_SIZE); + + tableView.getColumns().setAll(catColumn, countColumn); + + //register for category events + Category.registerListener(this); + } + + private SummaryTablePane() { + FXMLConstructor.construct(this, "SummaryTablePane.fxml"); + } + + public static synchronized SummaryTablePane getDefault() { + if (instance == null) { + instance = new SummaryTablePane(); + } + + return instance; + } + + /** listen to Category updates and rebuild the table */ + @Override + public void handleCategoryChanged(Collection<Long> ids) { + if (Case.isCaseOpen()) { + final ObservableList<Pair<Category, Integer>> data = FXCollections.observableArrayList(); + + for (Category cat : Category.values()) { + try { + data.add(new Pair<>(cat, EurekaController.getDefault().getGroupManager().getFileIDsWithCategory(cat).size())); + } catch (TskCoreException ex) { + Exceptions.printStackTrace(ex); + } + } + Platform.runLater(() -> { + tableView.setItems(data); + }); + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/GroupTreeCell.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/GroupTreeCell.java new file mode 100644 index 0000000000000000000000000000000000000000..f913978abf24d7fda0edfda06dfd448249994e32 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/GroupTreeCell.java @@ -0,0 +1,95 @@ +package org.sleuthkit.autopsy.imageanalyzer.gui.navpanel; + +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute; +import org.sleuthkit.autopsy.imageanalyzer.grouping.Grouping; +import javafx.application.Platform; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.scene.Node; +import javafx.scene.control.OverrunStyle; +import javafx.scene.control.Tooltip; +import javafx.scene.control.TreeCell; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import org.apache.commons.lang3.StringUtils; + +/** A {@link Node} in the tree that listens to its associated group. Manages + * visual representation of TreeNode in Tree. Listens to properties of group + * that don't impact hierarchy and updates ui to reflect them */ +class GroupTreeCell extends TreeCell<TreeNode> { + + /** icon to use if this cell's TreeNode doesn't represent a group but just a + * folder(with no DrawableFiles) in the hierarchy. */ + private static final Image EMPTY_FOLDER_ICON = new Image("org/sleuthkit/autopsy/imageanalyzer/images/folder.png"); + + public GroupTreeCell() { + //adjust indent, default is 10 which uses up a lot of space. + setStyle("-fx-indent:5;"); + //since end of path is probably more interesting put ellipsis at front + setTextOverrun(OverrunStyle.LEADING_ELLIPSIS); + } + + @Override + synchronized protected void updateItem(final TreeNode tNode, boolean empty) { + super.updateItem(tNode, empty); + prefWidthProperty().bind(getTreeView().widthProperty().subtract(15)); + + if (tNode != null) { + final String name = StringUtils.defaultIfBlank(tNode.getPath(), Grouping.UNKNOWN); + setTooltip(new Tooltip(name)); + + if (tNode.getGroup() == null) { + setText(name); + setGraphic(new ImageView(EMPTY_FOLDER_ICON)); + } else { + //this TreeNode has a group so append counts to name ... + setText(name + " (" + getNumerator() + getDenominator() + ")"); + + + //if number of files in this group changes (eg file is recategorized), update counts + tNode.getGroup().fileIds().addListener(new InvalidationListener() { + @Override + public void invalidated(Observable o) { + Platform.runLater(new Runnable() { + @Override + public void run() { + setText(name + " (" + getNumerator() + getDenominator() + ")"); + } + }); + } + }); + + //... and use icon corresponding to group type + setGraphic(new ImageView(tNode.getGroup().groupKey.getAttribute().getIcon())); + } + } else { + setTooltip(null); + setText(null); + setGraphic(null); + } + } + + /** @return the Numerator of the count to append to the group name = number + * of hashset hits + "/" */ + synchronized private String getNumerator() { + try { + final String numerator = (getItem().getGroup().groupKey.getAttribute() != DrawableAttribute.HASHSET) + ? getItem().getGroup().getFilesWithHashSetHitsCount() + "/" + : ""; + return numerator; + } catch (NullPointerException ex) { + return ""; + } + + } + + /** @return the Denominator of the count to append to the group name = + * number of files in group */ + synchronized private Integer getDenominator() { + try { + return getItem().getGroup().getSize(); + } catch (NullPointerException ex) { + return 0; + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/GroupTreeItem.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/GroupTreeItem.java new file mode 100644 index 0000000000000000000000000000000000000000..7dfcc5d47d63b01100988bbeb765efd068b3c882 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/GroupTreeItem.java @@ -0,0 +1,252 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-14 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.gui.navpanel; + +import org.sleuthkit.autopsy.imageanalyzer.grouping.Grouping; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.event.EventType; +import javafx.scene.control.TreeItem; +import org.apache.commons.lang3.StringUtils; +import org.sleuthkit.autopsy.coreutils.Logger; + +/** A node in the nav/hash tree. Manages inserts and removals. Has parents + * and children. Does not have graphical properties these are configured in + * {@link GroupTreeCell}. Each GroupTreeItem has a TreeNode which has a path + * segment and may or may not have a group */ +class GroupTreeItem extends TreeItem<TreeNode> implements Comparable<GroupTreeItem> { + + /** maps a path segment to the child item of this item with that path + * segment */ + private Map<String, GroupTreeItem> childMap = new HashMap<>(); + /** the comparator if any used to sort the children of this item */ + private TreeNodeComparators comp; + + public GroupTreeItem(String t, Grouping g, TreeNodeComparators comp) { + super(new TreeNode(t, g)); + this.comp = comp; + } + + /** Returns the full absolute path of this level in the tree + * + * @return the full absolute path of this level in the tree */ + public String getAbsolutePath() { + if (getParent() != null) { + return ((GroupTreeItem) getParent()).getAbsolutePath() + getValue().getPath() + "/"; + } else { + return getValue().getPath() + "/"; + } + } + + /** 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) + */ + void insert(String path, Grouping g, Boolean tree) { + if (tree) { + String cleanPath = StringUtils.stripStart(path, "/"); + + // get the first token + String prefix = StringUtils.substringBefore(cleanPath, "/"); + + // Are we at the end of the recursion? + if ("".equals(prefix)) { + getValue().setGroup(g); + } else { + GroupTreeItem prefixTreeItem = childMap.get(prefix); + if (prefixTreeItem == null) { + final GroupTreeItem newTreeItem = new GroupTreeItem(prefix, null, comp); + + prefixTreeItem = newTreeItem; + childMap.put(prefix, prefixTreeItem); + Platform.runLater(new Runnable() { + @Override + public void run() { + synchronized (getChildren()) { + getChildren().add(newTreeItem); + } + } + }); + + } + + // recursively go into the path + prefixTreeItem.insert(StringUtils.stripStart(cleanPath, prefix), g, tree); + } + } else { + GroupTreeItem treeItem = childMap.get(path); + if (treeItem == null) { + final GroupTreeItem newTreeItem = new GroupTreeItem(path, g, comp); + newTreeItem.setExpanded(true); + childMap.put(path, newTreeItem); + + Platform.runLater(new Runnable() { + @Override + public void run() { + synchronized (getChildren()) { + getChildren().add(newTreeItem); + if (comp != null) { + FXCollections.sort(getChildren(), comp); + } + } + } + }); + + } + } + } + + /** 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) + */ + void insert(List<String> path, Grouping g, Boolean tree) { + if (tree) { + // Are we at the end of the recursion? + if (path.isEmpty()) { + getValue().setGroup(g); + } else { + String prefix = path.get(0); + + GroupTreeItem prefixTreeItem = childMap.get(prefix); + if (prefixTreeItem == null) { + final GroupTreeItem newTreeItem = new GroupTreeItem(prefix, null, comp); + + prefixTreeItem = newTreeItem; + childMap.put(prefix, prefixTreeItem); + + Platform.runLater(new Runnable() { + @Override + public void run() { + synchronized (getChildren()) { + getChildren().add(newTreeItem); + } + } + }); + + } + + // recursively go into the path + prefixTreeItem.insert(path.subList(1, path.size()), g, tree); + } + } else { + //flat list + GroupTreeItem treeItem = childMap.get(StringUtils.join(path, "/")); + if (treeItem == null) { + final GroupTreeItem newTreeItem = new GroupTreeItem(StringUtils.join(path, "/"), g, comp); + newTreeItem.setExpanded(true); + childMap.put(path.get(0), newTreeItem); + + Platform.runLater(new Runnable() { + @Override + public void run() { + synchronized (getChildren()) { + getChildren().add(newTreeItem); + if (comp != null) { + FXCollections.sort(getChildren(), comp); + } + } + } + }); + } + } + } + + @Override + public int compareTo(GroupTreeItem o) { + return comp.compare(this, o); + } + + static GroupTreeItem getTreeItemForGroup(GroupTreeItem root, Grouping grouping) { + if (root.getValue().getGroup() == grouping) { + return root; + } else { + synchronized (root.getChildren()) { + for (TreeItem<TreeNode> child : root.getChildren()) { + final GroupTreeItem childGTI = (GroupTreeItem) child; + + GroupTreeItem val = getTreeItemForGroup(childGTI, grouping); + if (val != null) { + return val; + } + } + } + } + return null; + } + + GroupTreeItem getTreeItemForPath(List<String> path) { + // end of recursion + if (path.isEmpty()) { + return this; + } else { + synchronized (getChildren()) { + String prefix = path.get(0); + + GroupTreeItem prefixTreeItem = childMap.get(prefix); + if (prefixTreeItem == null) { + // @@@ ERROR; + return null; + } + + // recursively go into the path + return prefixTreeItem.getTreeItemForPath(path.subList(1, path.size())); + } + } + } + + void removeFromParent() { + final GroupTreeItem parent = (GroupTreeItem) getParent(); + if (parent != null) { + parent.childMap.remove(getValue().getPath()); + + Platform.runLater(new Runnable() { + @Override + public void run() { + synchronized (parent.getChildren()) { + parent.getChildren().removeAll(GroupTreeItem.this); + } + } + }); + + if (parent.childMap.isEmpty()) { + parent.removeFromParent(); + } + } + } + + void resortChildren(TreeNodeComparators newComp) { + this.comp = newComp; + synchronized (getChildren()) { + FXCollections.sort(getChildren(), comp); + } + for (GroupTreeItem ti : childMap.values()) { + ti.resortChildren(comp); + } + } +} \ No newline at end of file diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/NavPanel.fxml b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/NavPanel.fxml new file mode 100644 index 0000000000000000000000000000000000000000..100d08afa90711b89d1fca3dae486a06bd739b1a --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/NavPanel.fxml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import java.lang.*?> +<?import javafx.collections.*?> +<?import javafx.scene.control.*?> +<?import javafx.scene.image.*?> +<?import javafx.scene.layout.*?> + +<fx:root id="BorderPane" maxHeight="-1.0" maxWidth="-1.0" minHeight="-1.0" minWidth="200.0" prefHeight="-1.0" prefWidth="-1.0" type="javafx.scene.layout.BorderPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> + <center> + <TabPane fx:id="navTabPane" prefHeight="-1.0" prefWidth="-1.0" tabClosingPolicy="UNAVAILABLE"> + <tabs> + <Tab fx:id="navTab" text="Contents"> + <content> + <AnchorPane prefHeight="-1.0" prefWidth="-1.0"> + <children> + <TreeView fx:id="navTree" minWidth="-1.0" prefHeight="-1.0" prefWidth="-1.0" style="" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" /> + </children> + </AnchorPane> + </content> + <graphic> + <ImageView fitHeight="16.0" fitWidth="16.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true"> + <image> + <Image url="@../../images/folder-tree.png" /> + </image> + </ImageView> + </graphic> + </Tab> + <Tab fx:id="hashTab" text="Hash Hits"> + <content> + <AnchorPane id="Content" minHeight="0.0" minWidth="0.0" prefHeight="-1.0" prefWidth="-1.0"> + <children> + <BorderPane prefHeight="-1.0" prefWidth="-1.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"> + <center> + <TreeView fx:id="hashTree" prefHeight="-1.0" prefWidth="-1.0" /> + </center> + <top> + <ToolBar> + <items> + <Label text="Sort By:" /> + <ComboBox fx:id="sortByBox"> + <items> + <FXCollections fx:factory="observableArrayList"> + <String fx:value="Item 1" /> + <String fx:value="Item 2" /> + <String fx:value="Item 3" /> + </FXCollections> + </items> + </ComboBox> + </items> + </ToolBar> + </top> + </BorderPane> + </children> + </AnchorPane> + </content> + <graphic> + <ImageView fitHeight="16.0" fitWidth="16.0" mouseTransparent="true" pickOnBounds="true" preserveRatio="true"> + <image> + <Image url="@../../images/hashset_hits.png" /> + </image> + </ImageView> + </graphic> + </Tab> + </tabs> + </TabPane> + </center> +</fx:root> diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/NavPanel.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/NavPanel.java new file mode 100644 index 0000000000000000000000000000000000000000..a65766ae0c632af339a9470dae1a2b31d8df20fd --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/NavPanel.java @@ -0,0 +1,375 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.gui.navpanel; + +import org.sleuthkit.autopsy.imageanalyzer.EurekaController; +import org.sleuthkit.autopsy.imageanalyzer.FXMLConstructor; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableFile; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute; +import org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute.AttributeName; +import static org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute.AttributeName.PATH; +import static org.sleuthkit.autopsy.imageanalyzer.datamodel.DrawableAttribute.AttributeName.TAGS; +import org.sleuthkit.autopsy.imageanalyzer.grouping.Grouping; +import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupKey; +import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupSortBy; +import org.sleuthkit.autopsy.imageanalyzer.grouping.GroupViewState; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.ResourceBundle; +import javafx.application.Platform; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ComboBox; +import javafx.scene.control.ListView; +import javafx.scene.control.SelectionMode; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.control.TreeItem; +import javafx.scene.control.TreeView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import org.apache.commons.lang3.StringUtils; +import org.openide.util.Exceptions; +import org.sleuthkit.datamodel.TagName; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * 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. + */ +public class NavPanel extends BorderPane { + + @FXML + private ResourceBundle resources; + + @FXML + private URL location; + + /** + * TreeView for folders with hash hits + */ + @FXML + private TreeView<TreeNode> hashTree; + + @FXML + private TabPane navTabPane; + + /** + * TreeView for all folders + */ + @FXML + private TreeView<TreeNode> navTree; + + @FXML + private Tab hashTab; + + @FXML + private Tab navTab; + + @FXML + private ComboBox<TreeNodeComparators> sortByBox; + + /** contains the 'active tree' */ + private final SimpleObjectProperty<TreeView<TreeNode>> activeTreeProperty = new SimpleObjectProperty<>(); + + private GroupTreeItem navTreeRoot; + + private GroupTreeItem hashTreeRoot; + + public NavPanel(EurekaController controller) { + this.controller = controller; + FXMLConstructor.construct(this, "NavPanel.fxml"); + } + + @FXML + void initialize() { + assert hashTab != null : "fx:id=\"hashTab\" was not injected: check your FXML file 'NavPanel.fxml'."; + assert hashTree != null : "fx:id=\"hashTree\" was not injected: check your FXML file 'NavPanel.fxml'."; + assert navTab != null : "fx:id=\"navTab\" was not injected: check your FXML file 'NavPanel.fxml'."; + assert navTabPane != null : "fx:id=\"navTabPane\" was not injected: check your FXML file 'NavPanel.fxml'."; + assert navTree != null : "fx:id=\"navTree\" was not injected: check your FXML file 'NavPanel.fxml'."; + assert sortByBox != null : "fx:id=\"sortByBox\" was not injected: check your FXML file 'NavPanel.fxml'."; + + VBox.setVgrow(this, Priority.SOMETIMES); + + navTree.setShowRoot(false); + hashTree.setShowRoot(false); + + navTree.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); + hashTree.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); + + sortByBox.setCellFactory((ListView<TreeNodeComparators> p) -> new TreeNodeComparators.ComparatorListCell()); + + sortByBox.setButtonCell(new TreeNodeComparators.ComparatorListCell()); + sortByBox.setItems(FXCollections.observableArrayList(FXCollections.observableArrayList(TreeNodeComparators.values()))); + sortByBox.getSelectionModel().select(TreeNodeComparators.HIT_COUNT); + sortByBox.getSelectionModel().selectedItemProperty().addListener((Observable o) -> { + resortHashTree(); + }); + + navTree.setCellFactory((TreeView<TreeNode> p) -> new GroupTreeCell()); + + hashTree.setCellFactory((TreeView<TreeNode> p) -> new GroupTreeCell()); + + activeTreeProperty.addListener(new InvalidationListener() { + private void setSelectedGroupProperty() { + final TreeItem<TreeNode> selectedItem = activeTreeProperty.get().getSelectionModel().getSelectedItem(); + if (selectedItem != null && selectedItem.getValue() != null && selectedItem.getValue().getGroup() != null) { + controller.pushGroup(GroupViewState.tile(selectedItem.getValue().getGroup()), false); + } else { +// selectedGroupProperty.set(selectedItem.getValue().getGroup()); + } + } + + @Override + public void invalidated(Observable o) { + setSelectedGroupProperty(); + activeTreeProperty.get().getSelectionModel().selectedItemProperty().addListener((Observable o1) -> { + setSelectedGroupProperty(); + }); + } + }); + + this.activeTreeProperty.set(navTree); + + navTabPane.getSelectionModel().selectedItemProperty().addListener((ObservableValue<? extends Tab> ov, Tab t, Tab t1) -> { + if (t1 == hashTab) { + activeTreeProperty.set(hashTree); + } else if (t1 == navTab) { + activeTreeProperty.set(navTree); + } + }); + + initHashTree(); + initNavTree(); + + controller.getGroupManager().getAnalyzedGroups().addListener((ListChangeListener.Change<? extends Grouping> change) -> { + while (change.next()) { + for (Grouping g : change.getAddedSubList()) { + insertIntoNavTree(g); + if (g.getFilesWithHashSetHitsCount() > 0) { + insertIntoHashTree(g); + } + } + for (Grouping g : change.getRemoved()) { + removeFromNavTree(g); + removeFromHashTree(g); + } + } + }); + + controller.viewState().addListener((ObservableValue<? extends GroupViewState> observable, GroupViewState oldValue, GroupViewState newValue) -> { + if (newValue != null && newValue.getGroup() != null) { + setFocusedGroup(newValue.getGroup()); + } + }); + parentProperty().addListener(new RebuildTreeListener()); + + } + + private final EurekaController controller; + + synchronized private void rebuildNavTree() { + navTreeRoot = new GroupTreeItem("", null, sortByBox.getSelectionModel().selectedItemProperty().get()); + + ObservableList<Grouping> groups = controller.getGroupManager().getAnalyzedGroups(); + + for (Grouping g : groups) { + insertIntoNavTree(g); + } + + Platform.runLater(() -> { + navTree.setRoot(navTreeRoot); + navTreeRoot.setExpanded(true); + }); + } + + synchronized private void resortHashTree() { + hashTreeRoot.resortChildren(sortByBox.getSelectionModel().getSelectedItem()); + } + + synchronized private void rebuildHashTree() { + hashTreeRoot = new GroupTreeItem("", null, sortByBox.getSelectionModel().getSelectedItem()); + //TODO: can we do this as db query? + List<String> hashSetNames = controller.getGroupManager().findValuesForAttribute(DrawableAttribute.HASHSET, GroupSortBy.NONE); + for (String name : hashSetNames) { + try { + List<Long> fileIDsInGroup = controller.getGroupManager().getFileIDsInGroup(new GroupKey(DrawableAttribute.HASHSET, name)); + + for (Long fileId : fileIDsInGroup) { + + DrawableFile file = controller.getFileFromId(fileId); + Collection<GroupKey> groupKeysForFile; + if (controller.getGroupManager().getGroupBy() == DrawableAttribute.TAGS) { + Collection<TagName> tagNames = (Collection<TagName>) file.getValueOfAttribute(DrawableAttribute.TAGS); + groupKeysForFile = new ArrayList<>(); + for (TagName tn : tagNames) { + groupKeysForFile.add(new GroupKey(DrawableAttribute.TAGS, tn)); + } + } else { + groupKeysForFile = controller.getGroupManager().getGroupKeysForFile(file); + } + + for (GroupKey k : groupKeysForFile) { + final Grouping groupForKey = controller.getGroupManager().getGroupForKey(k); + if (groupForKey != null) { + insertIntoHashTree(groupForKey); + } + } + } + } catch (TskCoreException ex) { + Exceptions.printStackTrace(ex); + } + } + Platform.runLater(() -> { + hashTree.setRoot(hashTreeRoot); + hashTreeRoot.setExpanded(true); + }); + + } + + private void insertIntoHashTree(Grouping g) { + initHashTree(); + hashTreeRoot.insert(g.groupKey.getValueDisplayName(), g, false); + } + + /** + * Set the tree to the passed in group + * + * @param grouping + */ + public void setFocusedGroup(Grouping grouping) { + + List<String> path = groupingToPath(grouping); + + final GroupTreeItem treeItemForGroup = ((GroupTreeItem) activeTreeProperty.get().getRoot()).getTreeItemForPath(path); + + if (treeItemForGroup != null) { + Platform.runLater(() -> { + TreeItem ti = treeItemForGroup; + while (ti != null) { + ti.setExpanded(true); + ti = ti.getParent(); + } + int row = activeTreeProperty.get().getRow(treeItemForGroup); + if (row != -1) { + activeTreeProperty.get().getSelectionModel().select(treeItemForGroup); + activeTreeProperty.get().scrollTo(row); + + } + }); + } + } + + private static List<String> groupingToPath(Grouping g) { + + AttributeName attrName = g.groupKey.getAttribute().attrName; + String path = null; + switch (attrName) { + case PATH: + path = ((String) g.groupKey.getValue()); + break; + case TAGS: + path = ((TagName) g.groupKey.getValue()).getDisplayName(); + default: + if (path == null) { + path = g.groupKey.getValue().toString(); + } + } + + String cleanPath = StringUtils.stripStart(path, "/"); + String[] tokens = cleanPath.split("/"); + + return Arrays.asList(tokens); + } + + private void insertIntoNavTree(Grouping g) { + initNavTree(); + List<String> path = groupingToPath(g); + + AttributeName attrName = g.groupKey.getAttribute().attrName; + switch (attrName) { + case PATH: + navTreeRoot.insert(path, g, true); + break; + default: + navTreeRoot.insert(path, g, false); + break; + } + } + + private void removeFromNavTree(Grouping g) { + initNavTree(); + final GroupTreeItem treeItemForGroup = GroupTreeItem.getTreeItemForGroup(navTreeRoot, g); + if (treeItemForGroup != null) { + treeItemForGroup.removeFromParent(); + } + } + + private void removeFromHashTree(Grouping g) { + initHashTree(); + final GroupTreeItem treeItemForGroup = GroupTreeItem.getTreeItemForGroup(hashTreeRoot, g); + if (treeItemForGroup != null) { + treeItemForGroup.removeFromParent(); + } + } + + private void initNavTree() { + if (navTreeRoot == null) { + navTreeRoot = new GroupTreeItem("", null, null); + + Platform.runLater(() -> { + navTree.setRoot(navTreeRoot); + navTreeRoot.setExpanded(true); + }); + } + } + + private void initHashTree() { + if (hashTreeRoot == null) { + hashTreeRoot = new GroupTreeItem("", null, null); + + Platform.runLater(() -> { + hashTree.setRoot(hashTreeRoot); + hashTreeRoot.setExpanded(true); + }); + + } + } + + private class RebuildTreeListener implements InvalidationListener { + + @Override + public void invalidated(Observable o) { + rebuildHashTree(); + rebuildNavTree(); + } + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/TreeNode.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/TreeNode.java new file mode 100644 index 0000000000000000000000000000000000000000..6b9919dc84ca475e5f8ad537f31aa3a0fe421075 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/TreeNode.java @@ -0,0 +1,47 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.gui.navpanel; + +import org.sleuthkit.autopsy.imageanalyzer.grouping.Grouping; + +/** + * + */ + class TreeNode { + + private String path; + private Grouping group; + + public String getPath() { + return path; + } + + public Grouping getGroup() { + return group; + } + + public TreeNode(String path, Grouping group) { + this.path = path; + this.group = group; + } + + void setGroup(Grouping g) { + group = g; + } +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/TreeNodeComparators.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/TreeNodeComparators.java new file mode 100644 index 0000000000000000000000000000000000000000..bab4f5c6691e14c30db39ae74afc0b7ee15c7937 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/gui/navpanel/TreeNodeComparators.java @@ -0,0 +1,94 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-14 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.gui.navpanel; + +import java.util.Comparator; +import javafx.scene.control.ListCell; +import javafx.scene.control.TreeItem; + +/** + * + */ +enum TreeNodeComparators implements Comparator<TreeItem<TreeNode>>, NonNullCompareable { + + ALPHABETICAL("Group Name") { + @Override + public int nonNullCompare(TreeItem<TreeNode> o1, TreeItem<TreeNode> o2) { + + return o1.getValue().getGroup().groupKey.getValue().toString().compareTo(o2.getValue().getGroup().groupKey.getValue().toString()); + } + }, HIT_COUNT("Hit Count") { + @Override + public int nonNullCompare(TreeItem<TreeNode> o1, TreeItem<TreeNode> o2) { + + return -Integer.compare(o1.getValue().getGroup().getFilesWithHashSetHitsCount(), o2.getValue().getGroup().getFilesWithHashSetHitsCount()); + } + }, FILE_COUNT("Group Size") { + @Override + public int nonNullCompare(TreeItem<TreeNode> o1, TreeItem<TreeNode> o2) { + + return -Integer.compare(o1.getValue().getGroup().getSize(), o2.getValue().getGroup().getSize()); + } + }, HIT_FILE_RATIO("Hit Density") { + @Override + public int nonNullCompare(TreeItem<TreeNode> o1, TreeItem<TreeNode> o2) { + + return -Double.compare(o1.getValue().getGroup().getFilesWithHashSetHitsCount() / o1.getValue().getGroup().getSize().doubleValue(), + o2.getValue().getGroup().getFilesWithHashSetHitsCount() / o2.getValue().getGroup().getSize().doubleValue()); + } + }; + + @Override + public int compare(TreeItem<TreeNode> o1, TreeItem<TreeNode> o2) { + if (o1.getValue().getGroup() == null) { + if (o1.getValue().getGroup() == null) { + return 0; + } + return 1; + } else if (o2.getValue().getGroup() == null) { + return -1; + } + return nonNullCompare(o1, o2); + } + final private String displayName; + + public String getDisplayName() { + return displayName; + } + + private TreeNodeComparators(String displayName) { + this.displayName = displayName; + } + + public static class ComparatorListCell extends ListCell<TreeNodeComparators> { + + @Override + protected void updateItem(TreeNodeComparators t, boolean bln) { + super.updateItem(t, bln); + if (t != null) { + setText(t.getDisplayName()); + } + } + } +} + +interface NonNullCompareable { + + int nonNullCompare(TreeItem<TreeNode> o1, TreeItem<TreeNode> o2); +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/Clapperboard.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/Clapperboard.png new file mode 100644 index 0000000000000000000000000000000000000000..134ec3828be1031442406547f035af1e2c781538 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/Clapperboard.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/Folder-icon.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/Folder-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..77fb7695deb151621129a32a9d1b377851a65359 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/Folder-icon.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/TriangleDown.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/TriangleDown.png new file mode 100644 index 0000000000000000000000000000000000000000..ea290a20c72e077e8e5e39770c2da518ef26206e Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/TriangleDown.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/application_view_tile.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/application_view_tile.png new file mode 100644 index 0000000000000000000000000000000000000000..3bc0bd32fceb21d70368f7842a00a53d6369ba48 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/application_view_tile.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-090.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-090.png new file mode 100644 index 0000000000000000000000000000000000000000..f62345e491b0af46cf10f59d46467c257d5be8d9 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-090.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-180.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-180.png new file mode 100644 index 0000000000000000000000000000000000000000..4d2aa3ccb2d59ecf6cc1db2006bc330a7b32423c Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-180.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-270.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-270.png new file mode 100644 index 0000000000000000000000000000000000000000..56e9a63d6233853be9c14ec522652611d7b9056c Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-270.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-circle-double-135.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-circle-double-135.png new file mode 100644 index 0000000000000000000000000000000000000000..06f1ee390a69aede315c33ab6ba8c085bcdd003e Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-circle-double-135.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-circle-double.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-circle-double.png new file mode 100644 index 0000000000000000000000000000000000000000..1c5605506772be9ca53f69e424c4a37a98375c02 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-circle-double.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-resize-090.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-resize-090.png new file mode 100644 index 0000000000000000000000000000000000000000..dcfa5ab5364f6b4d0093a20d453df6e06bb8f06c Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-resize-090.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-resize.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-resize.png new file mode 100644 index 0000000000000000000000000000000000000000..a41414154515ed02b66ec3508436849ea927427b Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow-resize.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..12077d33242acb0933fb8a1eb1e9b283173bf3b6 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow_down.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow_down.png new file mode 100644 index 0000000000000000000000000000000000000000..2c4e279377bf348f9cf53894e76bb673ccf067bd Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow_down.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow_up.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow_up.png new file mode 100644 index 0000000000000000000000000000000000000000..1ebb193243780b8eb1919a51ef27c2a0d36ccec2 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/arrow_up.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/border-bottom-double.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/border-bottom-double.png new file mode 100644 index 0000000000000000000000000000000000000000..367ab664eb9283f66f5e33c83dff15411a02c29b Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/border-bottom-double.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/border-top-bottom-double.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/border-top-bottom-double.png new file mode 100644 index 0000000000000000000000000000000000000000..7cdfad4ae58a8b48ff96a5a26099fa03bbfec344 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/border-top-bottom-double.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/bullet_arrow_down.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/bullet_arrow_down.png new file mode 100644 index 0000000000000000000000000000000000000000..9b23c06d7b4f4689dc8c9fd4e9d4d1f199fe376f Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/bullet_arrow_down.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/camera.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/camera.png new file mode 100644 index 0000000000000000000000000000000000000000..8536d1a795888d8d396ac4211b639c6395dd08e6 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/camera.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/category-icon.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/category-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1a3f6a24aebe4cb403d45887605ed82a17fbfa19 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/category-icon.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/clock--minus.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/clock--minus.png new file mode 100644 index 0000000000000000000000000000000000000000..0a07b14b24d23b8b7443802447655722bd376325 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/clock--minus.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/clock--pencil.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/clock--pencil.png new file mode 100644 index 0000000000000000000000000000000000000000..6b89ab946efb92f31946a7c1f5edd7fd3ccdd674 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/clock--pencil.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/clock--plus.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/clock--plus.png new file mode 100644 index 0000000000000000000000000000000000000000..b3e5e329950eb2caaf33eb8cc39eab41980df824 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/clock--plus.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-000-small.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-000-small.png new file mode 100644 index 0000000000000000000000000000000000000000..826e3d7f03cfa0c62cf7c121b53e86ee2438d5b1 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-000-small.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-090-small.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-090-small.png new file mode 100644 index 0000000000000000000000000000000000000000..da6e413da1aaf7468f6dfca3ac1fa5e04a5ba7a0 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-090-small.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-090.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-090.png new file mode 100644 index 0000000000000000000000000000000000000000..f4db3cfd2592f9d0408792510132945b86978b6e Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-090.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-180-small.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-180-small.png new file mode 100644 index 0000000000000000000000000000000000000000..e4e0830f67db1b0bc85b2e6768aea911e1dc9ae9 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-180-small.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-180.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-180.png new file mode 100644 index 0000000000000000000000000000000000000000..6cb4ed8cb59ea4c3ece677295f99a105c98fb9dd Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-180.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-270-small.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-270-small.png new file mode 100644 index 0000000000000000000000000000000000000000..ab4753fbfd58cb3d7adafefbdde15d516826d9f1 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-270-small.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-270.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-270.png new file mode 100644 index 0000000000000000000000000000000000000000..8232606b0fbcac9ae9314f0502db46238a2a2a92 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double-270.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double.png new file mode 100644 index 0000000000000000000000000000000000000000..4197fb468badbb0a39a578739a99ab817d3bb9e8 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/control-double.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/film.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/film.png new file mode 100644 index 0000000000000000000000000000000000000000..31fa687ecd6474f13007b3b0a903ed5ee937a6b6 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/film.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/flag_gray.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/flag_gray.png new file mode 100644 index 0000000000000000000000000000000000000000..1888f8da96ea4b145de240a906f985540bcf8037 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/flag_gray.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/flag_red.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/flag_red.png new file mode 100644 index 0000000000000000000000000000000000000000..e8a602da7b17a323b2c9afe3d8aac62cb717a0b8 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/flag_red.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder-open-image.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder-open-image.png new file mode 100644 index 0000000000000000000000000000000000000000..6b21e1abbb7ba2fc1e13cea8ab799f3beedc3ac0 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder-open-image.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder-rename.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder-rename.png new file mode 100644 index 0000000000000000000000000000000000000000..0ae261d4d171cdfa71687b24e85c54129e5c187f Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder-rename.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder-tree.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder-tree.png new file mode 100644 index 0000000000000000000000000000000000000000..6812f52669d508ffcc1623b1e027f8463bd00d69 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder-tree.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder.png new file mode 100644 index 0000000000000000000000000000000000000000..784e8fa48234f4f64b6922a6758f254ee0ca08ec Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder_picture.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder_picture.png new file mode 100644 index 0000000000000000000000000000000000000000..052b33638eaa0f870a255bfdd5df5b79fb01a89e Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folder_picture.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folders-path.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folders-path.png new file mode 100644 index 0000000000000000000000000000000000000000..ea9cefe849c36a569ba9891c9e207e4d7e521d94 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/folders-path.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/funnel.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/funnel.png new file mode 100644 index 0000000000000000000000000000000000000000..1f69604528f29ca95e3b124de2849067797d839f Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/funnel.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/group.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/group.png new file mode 100644 index 0000000000000000000000000000000000000000..7fb4e1f1e1cd6ee67d33ffd24f09ddd5c3478bec Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/group.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/hashset_hits.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/hashset_hits.png new file mode 100644 index 0000000000000000000000000000000000000000..f1caff1f30333a7d3dc5ab278472b433a2e6f165 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/hashset_hits.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/information.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/information.png new file mode 100644 index 0000000000000000000000000000000000000000..5d353a191098dee1e1e8f4bdf99c7f4bccb0f292 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/information.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/lightbulb.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/lightbulb.png new file mode 100644 index 0000000000000000000000000000000000000000..d22fde8ba46eabd4335e4fa88077e80f96b92d62 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/lightbulb.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/page_white_stack.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/page_white_stack.png new file mode 100644 index 0000000000000000000000000000000000000000..44084add79b9a0fc3354d16bbd4b4b5ff8095da7 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/page_white_stack.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/prohibition.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/prohibition.png new file mode 100644 index 0000000000000000000000000000000000000000..18f151071ad328b247966fe9d743b19e6827e430 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/prohibition.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/right arrow.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/right arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..2e304ad84c772659762b15ee5b02e08254d482ca Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/right arrow.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/right_arrow_128.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/right_arrow_128.png new file mode 100644 index 0000000000000000000000000000000000000000..2c88990a852fb5a54087c28ae2f5b9285912dce5 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/right_arrow_128.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/shape_group.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/shape_group.png new file mode 100644 index 0000000000000000000000000000000000000000..bb2ff516d35dc9a92ed6ffdc79595d61513e65e3 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/shape_group.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/slide.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/slide.png new file mode 100644 index 0000000000000000000000000000000000000000..8f1ae4496e4f874742133d42941b2301d23fb95c Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/slide.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/tag_red.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/tag_red.png new file mode 100644 index 0000000000000000000000000000000000000000..6ebb37d25f58c68246d8ad6a015295dfa5367870 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/tag_red.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/video-file.png b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/video-file.png new file mode 100644 index 0000000000000000000000000000000000000000..c290609f59048e61cb28af42c4500709e342f437 Binary files /dev/null and b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/images/video-file.png differ diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/license-eureka.txt b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/license-eureka.txt new file mode 100644 index 0000000000000000000000000000000000000000..fb3e1b684a3502c1045b0aa4b372b145a785f4a1 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/license-eureka.txt @@ -0,0 +1,18 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-14 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ \ No newline at end of file diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/progress/ProgressAdapter.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/progress/ProgressAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..80ccece647f086ca664cf58e9921daaedec783fa --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/progress/ProgressAdapter.java @@ -0,0 +1,43 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.progress; + +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyStringProperty; +import javafx.concurrent.Worker; + +/** + * + */ +public interface ProgressAdapter { + + public double getProgress(); + + public ReadOnlyDoubleProperty progressProperty(); + + public String getMessage(); + + public ReadOnlyStringProperty messageProperty(); + + public Worker.State getState(); + + public ReadOnlyObjectProperty<Worker.State> stateProperty(); + +} diff --git a/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/progress/ProgressAdapterBase.java b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/progress/ProgressAdapterBase.java new file mode 100644 index 0000000000000000000000000000000000000000..d0fe1be6e2606d9b053db982e9ef072ae1b9da23 --- /dev/null +++ b/ImageAnalyzer/src/org/sleuthkit/autopsy/imageanalyzer/progress/ProgressAdapterBase.java @@ -0,0 +1,83 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.imageanalyzer.progress; + +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.concurrent.Worker; + +/** + * + */ +public abstract class ProgressAdapterBase implements ProgressAdapter { + + @Override + public double getProgress() { + return progress.get(); + } + + public final void updateProgress(Double workDone) { + this.progress.set(workDone); + } + + @Override + public String getMessage() { + return message.get(); + } + + public final void updateMessage(String Status) { + this.message.set(Status); + } + + SimpleObjectProperty<Worker.State> state = new SimpleObjectProperty(Worker.State.READY); + + SimpleDoubleProperty progress = new SimpleDoubleProperty(this, "pregress"); + + SimpleStringProperty message = new SimpleStringProperty(this, "status"); + + @Override + public SimpleDoubleProperty progressProperty() { + return progress; + } + + @Override + public SimpleStringProperty messageProperty() { + return message; + } + + @Override + public Worker.State getState() { + return state.get(); + } + + protected void updateState(Worker.State newState) { + state.set(newState); + } + + @Override + public ReadOnlyObjectProperty<Worker.State> stateProperty() { + return new ReadOnlyObjectWrapper<>(state.get()); + } + + public ProgressAdapterBase() { + } +}