diff --git a/bindings/java/ivy.xml b/bindings/java/ivy.xml
index b28852837433a512843384a0e5c180e337bd1282..5d5379cca5171fd195ed983f05944266425c9915 100644
--- a/bindings/java/ivy.xml
+++ b/bindings/java/ivy.xml
@@ -1,6 +1,11 @@
 <ivy-module version="2.0">
 	<info organisation="org.sleuthkit" module="datamodel"/>
 	<dependencies>
+		<dependency org="joda-time" name="joda-time" rev="2.4" />
+		<dependency org="com.google.guava" name="guava" rev="19.0"/>
+		<dependency org="org.apache.commons" name="commons-lang3" rev="3.0"/>
+		
+		
 		<dependency org="junit" name="junit" rev="4.8.2"/>
 		<dependency org="com.googlecode.java-diff-utils" name="diffutils" rev="1.2.1"/>
 		<dependency org="org.xerial" name="sqlite-jdbc" rev="3.8.11">
diff --git a/bindings/java/nbproject/project.xml b/bindings/java/nbproject/project.xml
index 0659e9e3adb3b38fbbc8aff5ba318ee51ac13d77..c314a9fa27ebc469d565ddf88442c117c84e0d32 100755
--- a/bindings/java/nbproject/project.xml
+++ b/bindings/java/nbproject/project.xml
@@ -30,14 +30,14 @@
             </folders>
             <ide-actions>
                 <action name="build">
-                    <target>dist</target>
+                    <target>dist-PostgreSQL</target>
                 </action>
                 <action name="clean">
                     <target>clean</target>
                 </action>
                 <action name="rebuild">
                     <target>clean</target>
-                    <target>dist</target>
+                    <target>dist-PostgreSQL</target>
                 </action>
                 <action name="run.single">
                     <script>nbproject/ide-file-targets.xml</script>
@@ -75,17 +75,17 @@
             <export>
                 <type>folder</type>
                 <location>build</location>
-                <build-target>dist</build-target>
+                <build-target>dist-PostgreSQL</build-target>
             </export>
             <export>
                 <type>folder</type>
                 <location>build</location>
-                <build-target>dist</build-target>
+                <build-target>dist-PostgreSQL</build-target>
             </export>
             <export>
                 <type>folder</type>
                 <location>test</location>
-                <build-target>dist</build-target>
+                <build-target>dist-PostgreSQL</build-target>
             </export>
             <view>
                 <items>
@@ -111,12 +111,12 @@
             </view>
             <subprojects/>
         </general-data>
-        <java-data xmlns="http://www.netbeans.org/ns/freeform-project-java/3">
+        <java-data xmlns="http://www.netbeans.org/ns/freeform-project-java/4">
             <compilation-unit>
                 <package-root>src</package-root>
-                <classpath mode="compile">lib;lib/diffutils-1.2.1.jar;lib/junit-4.8.2.jar;lib/postgresql-9.4-1201.jdbc41.jar;lib/c3p0-0.9.5.jar;lib/mchange-commons-java-0.2.9.jar;lib/c3p0-0.9.5-sources.jar;lib/c3p0-0.9.5-javadoc.jar</classpath>
+                <classpath mode="compile">lib;lib/diffutils-1.2.1.jar;lib/junit-4.8.2.jar;lib/postgresql-9.4-1201.jdbc41.jar;lib/c3p0-0.9.5.jar;lib/mchange-commons-java-0.2.9.jar;lib/c3p0-0.9.5-sources.jar;lib/c3p0-0.9.5-javadoc.jar;lib/joda-time-2.4.jar;lib/commons-lang3-3.0.jar;lib/guava-19.0.jar</classpath>
                 <built-to>build</built-to>
-                <source-level>1.6</source-level>
+                <source-level>1.8</source-level>
             </compilation-unit>
             <compilation-unit>
                 <package-root>test</package-root>
@@ -124,7 +124,7 @@
                 <classpath mode="compile">build;lib/diffutils-1.2.1.jar;lib/diffutils-1.2.1-javadoc.jar;lib/diffutils-1.2.1-sources.jar;lib/junit-4.8.2.jar</classpath>
                 <built-to>build</built-to>
                 <built-to>test</built-to>
-                <source-level>1.6</source-level>
+                <source-level>1.8</source-level>
             </compilation-unit>
         </java-data>
         <preferences xmlns="http://www.netbeans.org/ns/auxiliary-configuration-preferences/1">
diff --git a/bindings/java/src/org/sleuthkit/datamodel/Bundle_ja.properties b/bindings/java/src/org/sleuthkit/datamodel/Bundle_ja.properties
index ce3ccde6ae1b632fef5ea3cf152c47addac60541..253b9bdf8268f9180f7f4fe6519a7f18d684e6c8 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/Bundle_ja.properties
+++ b/bindings/java/src/org/sleuthkit/datamodel/Bundle_ja.properties
@@ -1,194 +1,194 @@
-BlackboardArtifact.tskGenInfo.text=\u4E00\u822C\u60C5\u5831
-BlackboardArtifact.tskWebBookmark.text=\u30A6\u30A7\u30D6\u30B5\u30A4\u30C8\u30D6\u30C3\u30AF\u30DE\u30FC\u30AF
-BlackboardArtifact.tskWebCookie.text=\u30A6\u30A7\u30D6cookie
-BlackboardArtifact.tskWebHistory.text=\u30A6\u30A7\u30D6\u5C65\u6B74
-BlackboardArtifact.tskWebDownload.text=\u30A6\u30A7\u30D6\u30C0\u30A6\u30F3\u30ED\u30FC\u30C9
-BlackboardArtifact.tsk.recentObject.text=\u6700\u8FD1\u958B\u3044\u305F\u30C9\u30AD\u30E5\u30E1\u30F3\u30C8
-BlackboardArtifact.tskGpsTrackpoint.text=GPS\u30C8\u30E9\u30C3\u30AF\u30DD\u30A4\u30F3\u30C8
-BlackboardArtifact.tskInstalledProg.text=\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u6E08\u307F\u30D7\u30ED\u30B0\u30E9\u30E0
-BlackboardArtifact.tskKeywordHits.text=\u30AD\u30FC\u30EF\u30FC\u30C9\u30D2\u30C3\u30C8
-BlackboardArtifact.tskHashsetHit.text=\u30CF\u30C3\u30B7\u30E5\u30BB\u30C3\u30C8\u30D2\u30C3\u30C8
-BlackboardArtifact.tskInterestingFileHit.text=\u7591\u308F\u3057\u3044\u30D5\u30A1\u30A4\u30EB
-BlackboardArtifact.tskEmailMsg.text=E\u30E1\u30FC\u30EB\u30E1\u30C3\u30BB\u30FC\u30B8
-BlackboardArtifact.tskExtractedText.text=\u62BD\u51FA\u3055\u308C\u305F\u30C6\u30AD\u30B9\u30C8
-BlackboardArtifact.tskWebSearchQuery.text=\u30A6\u30A7\u30D6\u691C\u7D22
-BlackboardArtifact.tskMetadataExif.text=EXIF\u30E1\u30BF\u30C7\u30FC\u30BF
-BlackboardArtifact.tagFile.text=\u30BF\u30B0\u4ED8\u3051\u3055\u308C\u305F\u30D5\u30A1\u30A4\u30EB
-BlackboardArtifact.tskTagArtifact.text=\u30BF\u30B0\u4ED8\u3051\u3055\u308C\u305F\u7D50\u679C
-BlackboardArtifact.tskOsInfo.text=\u30AA\u30DA\u30EC\u30FC\u30C6\u30A3\u30F3\u30B0\u30B7\u30B9\u30C6\u30E0\u60C5\u5831
-BlackboardArtifact.tskOsAccount.text=\u30AA\u30DA\u30EC\u30FC\u30C6\u30A3\u30F3\u30B0\u30B7\u30B9\u30C6\u30E0\u30E6\u30FC\u30B6\u30A2\u30AB\u30A6\u30F3\u30C8
-BlackboardArtifact.tskServiceAccount.text=\u30A2\u30AB\u30A6\u30F3\u30C8
-BlackboardArtifact.tskContact.text=\u30B3\u30F3\u30BF\u30AF\u30C8
-BlackboardArtifact.tskMessage.text=\u30E1\u30C3\u30BB\u30FC\u30B8
-BlackboardArtifact.tskCalllog.text=\u30B3\u30FC\u30EB\u30ED\u30B0
-BlackboardArtifact.tskCalendarEntry.text=\u30AB\u30EC\u30F3\u30C0\u30FC\u30A8\u30F3\u30C8\u30EA\u30FC
-BlackboardArtifact.tskSpeedDialEntry.text=\u30B9\u30D4\u30FC\u30C9\u30C0\u30A4\u30EB\u30A8\u30F3\u30C8\u30EA\u30FC
-BlackboardArtifact.tskBluetoothPairing.text=Bluetooth\u30DA\u30A2\u30EA\u30F3\u30B0
-BlackboardArtifact.tskGpsBookmark.text=GPS\u30D6\u30C3\u30AF\u30DE\u30FC\u30AF
-BlackboardArtifact.tskGpsLastKnownLocation.text=\u6700\u5F8C\u306B\u53D6\u5F97\u3057\u305FGPS\u4F4D\u7F6E\u60C5\u5831
-BlackboardArtifact.tskGpsSearch.text=GPS\u691C\u7D22
-BlackboardArtifact.tskDeviceAttached.text=\u4ED8\u5C5E\u6A5F\u5668
-BlackboardArtifact.tskToolOutput.text=\u30ED\u30FC\u30C4\u30FC\u30EB\u30A2\u30A6\u30C8\u30D7\u30C3\u30C8
-BlackboardArtifact.tskProgRun.text=\u5B9F\u884C\u30D7\u30ED\u30B0\u30E9\u30E0
-BlackboardArtifact.tskEncryptionDetected.text=\u6697\u53F7\u5316\u691C\u51FA\u6E08
-BlackboardArtifact.tskExtMismatchDetected.text=\u62E1\u5F35\u5B50\u4E0D\u4E00\u81F4\u691C\u51FA\u6E08
-BlackboardArtifact.tskInterestingArtifactHit.text=\u7591\u308F\u3057\u3044\u7D50\u679C
+BlackboardArtifact.tskGenInfo.text=\u4e00\u822c\u60c5\u5831
+BlackboardArtifact.tskWebBookmark.text=\u30a6\u30a7\u30d6\u30b5\u30a4\u30c8\u30d6\u30c3\u30af\u30de\u30fc\u30af
+BlackboardArtifact.tskWebCookie.text=\u30a6\u30a7\u30d6cookie
+BlackboardArtifact.tskWebHistory.text=\u30a6\u30a7\u30d6\u5c65\u6b74
+BlackboardArtifact.tskWebDownload.text=\u30a6\u30a7\u30d6\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9
+BlackboardArtifact.tsk.recentObject.text=\u6700\u8fd1\u958b\u3044\u305f\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8
+BlackboardArtifact.tskGpsTrackpoint.text=GPS\u30c8\u30e9\u30c3\u30af\u30dd\u30a4\u30f3\u30c8
+BlackboardArtifact.tskInstalledProg.text=\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u6e08\u307f\u30d7\u30ed\u30b0\u30e9\u30e0
+BlackboardArtifact.tskKeywordHits.text=\u30ad\u30fc\u30ef\u30fc\u30c9\u30d2\u30c3\u30c8
+BlackboardArtifact.tskHashsetHit.text=\u30cf\u30c3\u30b7\u30e5\u30bb\u30c3\u30c8\u30d2\u30c3\u30c8
+BlackboardArtifact.tskInterestingFileHit.text=\u7591\u308f\u3057\u3044\u30d5\u30a1\u30a4\u30eb
+BlackboardArtifact.tskEmailMsg.text=E\u30e1\u30fc\u30eb\u30e1\u30c3\u30bb\u30fc\u30b8
+BlackboardArtifact.tskExtractedText.text=\u62bd\u51fa\u3055\u308c\u305f\u30c6\u30ad\u30b9\u30c8
+BlackboardArtifact.tskWebSearchQuery.text=\u30a6\u30a7\u30d6\u691c\u7d22
+BlackboardArtifact.tskMetadataExif.text=EXIF\u30e1\u30bf\u30c7\u30fc\u30bf
+BlackboardArtifact.tagFile.text=\u30bf\u30b0\u4ed8\u3051\u3055\u308c\u305f\u30d5\u30a1\u30a4\u30eb
+BlackboardArtifact.tskTagArtifact.text=\u30bf\u30b0\u4ed8\u3051\u3055\u308c\u305f\u7d50\u679c
+BlackboardArtifact.tskOsInfo.text=\u30aa\u30da\u30ec\u30fc\u30c6\u30a3\u30f3\u30b0\u30b7\u30b9\u30c6\u30e0\u60c5\u5831
+BlackboardArtifact.tskOsAccount.text=\u30aa\u30da\u30ec\u30fc\u30c6\u30a3\u30f3\u30b0\u30b7\u30b9\u30c6\u30e0\u30e6\u30fc\u30b6\u30a2\u30ab\u30a6\u30f3\u30c8
+BlackboardArtifact.tskServiceAccount.text=\u30a2\u30ab\u30a6\u30f3\u30c8
+BlackboardArtifact.tskContact.text=\u30b3\u30f3\u30bf\u30af\u30c8
+BlackboardArtifact.tskMessage.text=\u30e1\u30c3\u30bb\u30fc\u30b8
+BlackboardArtifact.tskCalllog.text=\u30b3\u30fc\u30eb\u30ed\u30b0
+BlackboardArtifact.tskCalendarEntry.text=\u30ab\u30ec\u30f3\u30c0\u30fc\u30a8\u30f3\u30c8\u30ea\u30fc
+BlackboardArtifact.tskSpeedDialEntry.text=\u30b9\u30d4\u30fc\u30c9\u30c0\u30a4\u30eb\u30a8\u30f3\u30c8\u30ea\u30fc
+BlackboardArtifact.tskBluetoothPairing.text=Bluetooth\u30da\u30a2\u30ea\u30f3\u30b0
+BlackboardArtifact.tskGpsBookmark.text=GPS\u30d6\u30c3\u30af\u30de\u30fc\u30af
+BlackboardArtifact.tskGpsLastKnownLocation.text=\u6700\u5f8c\u306b\u53d6\u5f97\u3057\u305fGPS\u4f4d\u7f6e\u60c5\u5831
+BlackboardArtifact.tskGpsSearch.text=GPS\u691c\u7d22
+BlackboardArtifact.tskDeviceAttached.text=\u4ed8\u5c5e\u6a5f\u5668
+BlackboardArtifact.tskToolOutput.text=\u30ed\u30fc\u30c4\u30fc\u30eb\u30a2\u30a6\u30c8\u30d7\u30c3\u30c8
+BlackboardArtifact.tskProgRun.text=\u5b9f\u884c\u30d7\u30ed\u30b0\u30e9\u30e0
+BlackboardArtifact.tskEncryptionDetected.text=\u6697\u53f7\u5316\u691c\u51fa\u6e08
+BlackboardArtifact.tskExtMismatchDetected.text=\u62e1\u5f35\u5b50\u4e0d\u4e00\u81f4\u691c\u51fa\u6e08
+BlackboardArtifact.tskInterestingArtifactHit.text=\u7591\u308f\u3057\u3044\u7d50\u679c
 BlackboardAttribute.tskUrl.text=URL
-BlackboardAttribute.tskDatetime.text=\u65E5\u4ED8\uFF0F\u6642\u523B
-BlackboardAttribute.tskName.text=\u540D\u524D
-BlackboardAttribute.tskProgName.text=\u30D7\u30ED\u30B0\u30E9\u30E0\u540D
-BlackboardAttribute.tskValue.text=\u30D0\u30EA\u30E5\u30FC
-BlackboardAttribute.tskFlag.text=\u30D5\u30E9\u30B0
-BlackboardAttribute.tskPath.text=\u30D1\u30B9
-BlackboardAttribute.tskKeyword.text=\u30AD\u30FC\u30EF\u30FC\u30C9
-BlackboardAttribute.tskKeywordRegexp.text=\u6B63\u898F\u8868\u73FE\u30AD\u30FC\u30EF\u30FC\u30C9
-BlackboardAttribute.tskKeywordPreview.text=\u30AD\u30FC\u30EF\u30FC\u30C9\u30D7\u30EC\u30D3\u30E5\u30FC
-BlackboardAttribute.tskKeywordSet.text=\u30AD\u30FC\u30EF\u30FC\u30C9\u30BB\u30C3\u30C8
-BlackboardAttribute.tskUserName.text=\u30E6\u30FC\u30B6\u540D
-BlackboardAttribute.tskDomain.text=\u30C9\u30E1\u30A4\u30F3
-BlackboardAttribute.tskPassword.text=\u30D1\u30B9\u30EF\u30FC\u30C9
-BlackboardAttribute.tskNamePerson.text=\u4EBA\u540D
-BlackboardAttribute.tskDeviceModel.text=\u6A5F\u5668\u30E2\u30C7\u30EB
-BlackboardAttribute.tskDeviceId.text=\u6A5F\u5668ID
-BlackboardAttribute.tskEmail.text=E\u30E1\u30FC\u30EB
-BlackboardAttribute.tskHashMd5.text=MD5\u30CF\u30C3\u30B7\u30E5
-BlackboardAttribute.tskHashSha1.text=SHA1\u30CF\u30C3\u30B7\u30E5
-BlackboardAttribute.tskHashSha225.text=SHA2-256\u30CF\u30C3\u30B7\u30E5
-BlackboardAttribute.tskHashSha2512.text=SHA2-512\u30CF\u30C3\u30B7\u30E5
-BlackboardAttribute.tskText.text=\u30C6\u30AD\u30B9\u30C8
-BlackboardAttribute.tskTextFile.text=\u30C6\u30AD\u30B9\u30C8\u30D5\u30A1\u30A4\u30EB
-BlackboardAttribute.tskTextLanguage.text=\u30C6\u30AD\u30B9\u30C8\u8A00\u8A9E
-BlackboardAttribute.tskEntropy.text=\u30A8\u30F3\u30C8\u30ED\u30D4\u30FC
-BlackboardAttribute.tskHashsetName.text=\u30CF\u30C3\u30B7\u30E5\u30BB\u30C3\u30C8\u540D
-BlackboardAttribute.tskInterestingFile.text=\u7591\u308F\u3057\u3044\u30D5\u30A1\u30A4\u30EB
-BlackboardAttribute.tskReferrer.text=\u30EA\u30D5\u30A1\u30E9
-BlackboardAttribute.tskDateTimeAccessed.text=\u30A2\u30AF\u30BB\u30B9\u65E5\u4ED8
-BlackboardAttribute.tskIpAddress.text=IP\u30A2\u30C9\u30EC\u30B9
-BlackboardAttribute.tskPhoneNumber.text=\u96FB\u8A71\u756A\u53F7
-BlackboardAttribute.tskPathId.text=\u30D1\u30B9ID
-BlackboardAttribute.tskSetName.text=\u30BB\u30C3\u30C8\u540D
-BlackboardAttribute.tskEncryptionDetected.text=\u6697\u53F7\u5316\u691C\u51FA\u6E08
-BlackboardAttribute.tskMalwareDetected.text=\u30DE\u30EB\u30A6\u30A7\u30A2\u691C\u51FA\u6E08
-BlackboardAttribute.tskDeviceMake.text=\u6A5F\u5668\u30E1\u30FC\u30AB\u30FC
-BlackboardAttribute.tskStegDetected.text=\u30B9\u30C6\u30AC\u30CE\u30B0\u30E9\u30D5\u30A3\u30FC\u691C\u51FA\u6E08
-BlackboardAttribute.tskEmailTo.text=E\u30E1\u30FC\u30EB\u5B9B\u5148
+BlackboardAttribute.tskDatetime.text=\u65e5\u4ed8\uff0f\u6642\u523b
+BlackboardAttribute.tskName.text=\u540d\u524d
+BlackboardAttribute.tskProgName.text=\u30d7\u30ed\u30b0\u30e9\u30e0\u540d
+BlackboardAttribute.tskValue.text=\u30d0\u30ea\u30e5\u30fc
+BlackboardAttribute.tskFlag.text=\u30d5\u30e9\u30b0
+BlackboardAttribute.tskPath.text=\u30d1\u30b9
+BlackboardAttribute.tskKeyword.text=\u30ad\u30fc\u30ef\u30fc\u30c9
+BlackboardAttribute.tskKeywordRegexp.text=\u6b63\u898f\u8868\u73fe\u30ad\u30fc\u30ef\u30fc\u30c9
+BlackboardAttribute.tskKeywordPreview.text=\u30ad\u30fc\u30ef\u30fc\u30c9\u30d7\u30ec\u30d3\u30e5\u30fc
+BlackboardAttribute.tskKeywordSet.text=\u30ad\u30fc\u30ef\u30fc\u30c9\u30bb\u30c3\u30c8
+BlackboardAttribute.tskUserName.text=\u30e6\u30fc\u30b6\u540d
+BlackboardAttribute.tskDomain.text=\u30c9\u30e1\u30a4\u30f3
+BlackboardAttribute.tskPassword.text=\u30d1\u30b9\u30ef\u30fc\u30c9
+BlackboardAttribute.tskNamePerson.text=\u4eba\u540d
+BlackboardAttribute.tskDeviceModel.text=\u6a5f\u5668\u30e2\u30c7\u30eb
+BlackboardAttribute.tskDeviceId.text=\u6a5f\u5668ID
+BlackboardAttribute.tskEmail.text=E\u30e1\u30fc\u30eb
+BlackboardAttribute.tskHashMd5.text=MD5\u30cf\u30c3\u30b7\u30e5
+BlackboardAttribute.tskHashSha1.text=SHA1\u30cf\u30c3\u30b7\u30e5
+BlackboardAttribute.tskHashSha225.text=SHA2-256\u30cf\u30c3\u30b7\u30e5
+BlackboardAttribute.tskHashSha2512.text=SHA2-512\u30cf\u30c3\u30b7\u30e5
+BlackboardAttribute.tskText.text=\u30c6\u30ad\u30b9\u30c8
+BlackboardAttribute.tskTextFile.text=\u30c6\u30ad\u30b9\u30c8\u30d5\u30a1\u30a4\u30eb
+BlackboardAttribute.tskTextLanguage.text=\u30c6\u30ad\u30b9\u30c8\u8a00\u8a9e
+BlackboardAttribute.tskEntropy.text=\u30a8\u30f3\u30c8\u30ed\u30d4\u30fc
+BlackboardAttribute.tskHashsetName.text=\u30cf\u30c3\u30b7\u30e5\u30bb\u30c3\u30c8\u540d
+BlackboardAttribute.tskInterestingFile.text=\u7591\u308f\u3057\u3044\u30d5\u30a1\u30a4\u30eb
+BlackboardAttribute.tskReferrer.text=\u30ea\u30d5\u30a1\u30e9
+BlackboardAttribute.tskDateTimeAccessed.text=\u30a2\u30af\u30bb\u30b9\u65e5\u4ed8
+BlackboardAttribute.tskIpAddress.text=IP\u30a2\u30c9\u30ec\u30b9
+BlackboardAttribute.tskPhoneNumber.text=\u96fb\u8a71\u756a\u53f7
+BlackboardAttribute.tskPathId.text=\u30d1\u30b9ID
+BlackboardAttribute.tskSetName.text=\u30bb\u30c3\u30c8\u540d
+BlackboardAttribute.tskEncryptionDetected.text=\u6697\u53f7\u5316\u691c\u51fa\u6e08
+BlackboardAttribute.tskMalwareDetected.text=\u30de\u30eb\u30a6\u30a7\u30a2\u691c\u51fa\u6e08
+BlackboardAttribute.tskDeviceMake.text=\u6a5f\u5668\u30e1\u30fc\u30ab\u30fc
+BlackboardAttribute.tskStegDetected.text=\u30b9\u30c6\u30ac\u30ce\u30b0\u30e9\u30d5\u30a3\u30fc\u691c\u51fa\u6e08
+BlackboardAttribute.tskEmailTo.text=E\u30e1\u30fc\u30eb\u5b9b\u5148
 BlackboardAttribute.tskEmailCc.text=E-Mail CC
 BlackboardAttribute.tskEmailBcc.text=E-Mail BCC
-BlackboardAttribute.tskEmailFrom.text=\u9001\u4FE1\u5143E\u30E1\u30FC\u30EB
-BlackboardAttribute.tskEmailContentPlain.text=\u30E1\u30C3\u30BB\u30FC\u30B8\uFF08\u30D7\u30EC\u30FC\u30F3\u30C6\u30AD\u30B9\u30C8\uFF09
-BlackboardAttribute.tskEmailContentHtml.text=\u30E1\u30C3\u30BB\u30FC\u30B8\uFF08HTML\uFF09
-BlackboardAttribute.tskEmailContentRtf.text=\u30E1\u30C3\u30BB\u30FC\u30B8\uFF08RTF\uFF09
-BlackboardAttribute.tskMsgId.text=\u30E1\u30C3\u30BB\u30FC\u30B8ID
-BlackboardAttribute.tskMsgReplyId.text=\u30E1\u30C3\u30BB\u30FC\u30B8\u30EA\u30D7\u30E9\u30A4ID
-BlackboardAttribute.tskDateTimeRcvd.text=\u53D7\u4FE1\u65E5
-BlackboardAttribute.tskDateTimeSent.text=\u9001\u4FE1\u65E5
-BlackboardAttribute.tskSubject.text=\u30B5\u30D6\u30B8\u30A7\u30AF\u30C8
-BlackboardAttribute.tskTitle.text=\u30BF\u30A4\u30C8\u30EB
-BlackboardAttribute.tskGeoLatitude.text=\u7DEF\u5EA6
-BlackboardAttribute.tskGeoLongitude.text=\u7D4C\u5EA6
-BlackboardAttribute.tskGeoVelocity.text=\u901F\u5EA6
-BlackboardAttribute.tskGeoAltitude.text=\u6A19\u9AD8
-BlackboardAttribute.tskGeoBearing.text=\u65B9\u5411
-BlackboardAttribute.tskGeoHPrecision.text=\u6C34\u5E73\u7CBE\u5EA6
-BlackboardAttribute.tskGeoVPrecision.text=\u5782\u76F4\u7CBE\u5EA6
-BlackboardAttribute.tskGeoMapDatum.text=\u6E2C\u5730\u7CFB
-BlackboardAttribute.tskFileTypeSig.text=\u30D5\u30A1\u30A4\u30EB\u30BF\u30A4\u30D7\uFF08\u30B7\u30B0\u30CD\u30C1\u30E3\uFF09
-BlackboardAttribute.tskFileTypeExt.text=\u30D5\u30A1\u30A4\u30EB\u30BF\u30A4\u30D7\uFF08\u62E1\u5F35\u5B50\uFF09
-BlackboardAttribute.tskTaggedArtifact.text=\u30BF\u30B0\u4ED8\u3051\u3055\u308C\u305F\u7D50\u679C
-BlackboardAttribute.tskTagName.text=\u30BF\u30B0\u540D
-BlackboardAttribute.tskComment.text=\u30B3\u30E1\u30F3\u30C8
-BlackboardAttribute.tskUrlDecoded.text=\u5FA9\u53F7\u5316\u3055\u308C\u305FURL
-BlackboardAttribute.tskDateTimeCreated.text=\u4F5C\u6210\u65E5
-BlackboardAttribute.tskDateTimeModified.text=\u4FEE\u6B63\u65E5
-BlackboardAttribute.tskProcessorArchitecture.text=\u30D7\u30ED\u30BB\u30C3\u30B5\u30A2\u30FC\u30AD\u30C6\u30AF\u30C1\u30E3
-BlackboardAttribute.tskVersion.text=\u30D0\u30FC\u30B8\u30E7\u30F3
-BlackboardAttribute.tskUserId.text=\u30E6\u30FC\u30B6ID
-BlackboardAttribute.tskDescription.text=\u8AAC\u660E
-BlackboardAttribute.tskMessageType.text=\u30E1\u30C3\u30BB\u30FC\u30B8\u30BF\u30A4\u30D7
-BlackboardAttribute.tskPhoneNumberHome.text=\u96FB\u8A71\u756A\u53F7\uFF08\u81EA\u5B85\uFF09
-BlackboardAttribute.tskPhoneNumberOffice.text=\u96FB\u8A71\u756A\u53F7\uFF08\u4F1A\u793E\uFF09
-BlackboardAttribute.tskPhoneNumberMobile.text=\u96FB\u8A71\u756A\u53F7\uFF08\u643A\u5E2F\uFF09
-BlackboardAttribute.tskPhoneNumberFrom.text=\u767A\u4FE1\u8005\u96FB\u8A71\u756A\u53F7
-BlackboardAttribute.tskPhoneNumberTo.text=\u7740\u4FE1\u8005\u96FB\u8A71\u756A\u53F7
-BlackboardAttribute.tskDirection.text=\u65B9\u5411
-BlackboardAttribute.tskEmailHome.text=E\u30E1\u30FC\u30EB\uFF08\u81EA\u5B85\uFF09
-BlackboardAttribute.tskEmailOffice.text=E\u30E1\u30FC\u30EB\uFF08\u30AA\u30D5\u30A3\u30B9\uFF09
-BlackboardAttribute.tskDateTimeStart.text=\u958B\u59CB\u65E5\u4ED8\uFF0F\u6642\u523B
-BlackboardAttribute.tskDateTimeEnd.text=\u7D42\u4E86\u65E5\u4ED8\uFF0F\u6642\u523B
-BlackboardAttribute.tskCalendarEntryType.text=\u30AB\u30EC\u30F3\u30C0\u30FC\u30A8\u30F3\u30C8\u30EA\u30FC\u30BF\u30A4\u30D7
-BlackboardAttribute.tskLocation.text=\u30ED\u30B1\u30FC\u30B7\u30E7\u30F3
-BlackboardAttribute.tskShortcut.text=\u30B7\u30E7\u30FC\u30C8\u30AB\u30C3\u30C8
-BlackboardAttribute.tskDeviceName.text=\u6A5F\u5668\u540D
-BlackboardAttribute.tskCategory.text=\u30AB\u30C6\u30B4\u30EA\u30FC
-BlackboardAttribute.tskEmailReplyTo.text=\u8FD4\u4FE1\u30A2\u30C9\u30EC\u30B9
-BlackboardAttribute.tskServerName.text=\u30B5\u30FC\u30D0\u540D
-BlackboardAttribute.tskCount.text=\u30AB\u30A6\u30F3\u30C8
-BlackboardAttribute.tskMinCount.text=\u6700\u5C0F\u30AB\u30A6\u30F3\u30C8
-BlackboardAttribute.tskPathSource.text=\u30D1\u30B9\u30BD\u30FC\u30B9
-BlackboardAttribute.tskPermissions.text=\u30D1\u30FC\u30DF\u30C3\u30B7\u30E7\u30F3
-BlackboardAttribute.tskAssociatedArtifact.text=\u95A2\u9023\u3059\u308B\u30A2\u30FC\u30C6\u30A3\u30D5\u30A1\u30AF\u30C8
-BlackboardAttribute.tskIsDeleted.text=\u306F\u524A\u9664\u3055\u308C\u307E\u3057\u305F
-AbstractFile.readLocal.exception.msg4.text=\u30D5\u30A1\u30A4\u30EB{0}\u306E\u8AAD\u307F\u53D6\u308A\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F
-AbstractFile.readLocal.exception.msg1.text=\u30ED\u30FC\u30AB\u30EB\u30D5\u30A1\u30A4\u30EB\u306E\u8AAD\u307F\u53D6\u308A\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002\u30ED\u30FC\u30AB\u30EB\u30D1\u30B9\u304C\u30BB\u30C3\u30C8\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002
-AbstractFile.readLocal.exception.msg2.text=\u30ED\u30FC\u30AB\u30EB\u30D5\u30A1\u30A4\u30EB\u306E\u8AAD\u307F\u53D6\u308A\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002\u4E0B\u8A18\u306E\u30ED\u30FC\u30AB\u30EB\u30D1\u30B9\u306B\u306F\u5B58\u5728\u3057\u307E\u305B\u3093\uFF1A{0}
-AbstractFile.readLocal.exception.msg3.text=\u30ED\u30FC\u30AB\u30EB\u30D5\u30A1\u30A4\u30EB\u306E\u8AAD\u307F\u53D6\u308A\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002\u4E0B\u8A18\u306E\u30ED\u30FC\u30AB\u30EB\u30D1\u30B9\u3067\u306F\u8AAD\u307F\u53D6\u308A\u3067\u304D\u307E\u305B\u3093\uFF1A{0}
-AbstractFile.readLocal.exception.msg5.text=\u30ED\u30FC\u30AB\u30EB\u30D5\u30A1\u30A4\u30EB{0}\u3092\u8AAD\u307F\u53D6\u308C\u307E\u305B\u3093
-FsContent.readInt.err.context.text=\u30A4\u30E1\u30FC\u30B8\u30D5\u30A1\u30A4\u30EB\u306E\u8AAD\u307F\u53D6\u308A\u30A8\u30E9\u30FC
-FsContent.readInt.err.msg.text=\u30A4\u30E1\u30FC\u30B8\u30D5\u30A1\u30A4\u30EB\u306F\u5B58\u5728\u3057\u306A\u3044\u304B\u3001\u30A2\u30AF\u30BB\u30B9\u304C\u62D2\u5426\u3055\u308C\u307E\u3057\u305F\u3002
-Image.verifyImageSize.errStr1.text=\n\u4E0D\u5B8C\u5168\u306A\u30A4\u30E1\u30FC\u30B8\u306E\u53EF\u80FD\u6027\uFF1A\u30AA\u30D5\u30BB\u30C3\u30C8{0}\u306B\u57FA\u3065\u3044\u305F\u30DC\u30EA\u30E5\u30FC\u30E0\u306E\u8AAD\u307F\u53D6\u308A\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F
-Image.verifyImageSize.errStr2.text=\n\u4E0D\u5B8C\u5168\u306A\u30A4\u30E1\u30FC\u30B8\u306E\u53EF\u80FD\u6027\uFF1A\u30AA\u30D5\u30BB\u30C3\u30C8{0}\u306B\u57FA\u3065\u3044\u305F\u30DC\u30EA\u30E5\u30FC\u30E0\u306E\u8AAD\u307F\u53D6\u308A\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F
-Image.verifyImageSize.errStr3.text=\n\u4E0D\u5B8C\u5168\u306A\u30A4\u30E1\u30FC\u30B8\u306E\u53EF\u80FD\u6027\uFF1A\u30AA\u30D5\u30BB\u30C3\u30C8{0}\u306B\u57FA\u3065\u3044\u305F\u30D5\u30A1\u30A4\u30EB\u30B7\u30B9\u30C6\u30E0\u306E\u8AAD\u307F\u53D6\u308A\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F
-Image.verifyImageSize.errStr4.text=\n\u4E0D\u5B8C\u5168\u306A\u30A4\u30E1\u30FC\u30B8\u306E\u53EF\u80FD\u6027\uFF1A\u30AA\u30D5\u30BB\u30C3\u30C8{0}\u306B\u57FA\u3065\u3044\u305F\u30D5\u30A1\u30A4\u30EB\u30B7\u30B9\u30C6\u30E0\u306E\u8AAD\u307F\u53D6\u308A\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F
-SleuthkitCase.isFileFromSource.exception.msg.text=\u30A8\u30E9\u30FC\uFF1A\u30C7\u30FC\u30BF\u30BD\u30FC\u30B9\u306F\u30DA\u30A2\u30EC\u30F3\u30C8\u304C\u7121\u3044\uFF08\u30A4\u30E1\u30FC\u30B8\u3001\u30D5\u30A1\u30A4\u30EB\u30BB\u30C3\u30C8\uFF09\u306F\u305A\u3067\u3059\u304C\u3001\u4E0B\u8A18\u304C\u5B58\u5728\u3057\u307E\u3059\uFF1A{0}
-SleuthkitCase.findFiles.exception.msg3.text=\u30D5\u30A1\u30A4\u30EB\u540D\u306B\u57FA\u3065\u3044\u305F\u30C7\u30FC\u30BF\u30BD\u30FC\u30B9\u306E\u691C\u7D22\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002
-SleuthkitCase.findFiles3.exception.msg1.text=\u30A8\u30E9\u30FC\uFF1A\u30C7\u30FC\u30BF\u30BD\u30FC\u30B9\u306F\u30DA\u30A2\u30EC\u30F3\u30C8\u304C\u7121\u3044\uFF08\u30A4\u30E1\u30FC\u30B8\u3001\u30D5\u30A1\u30A4\u30EB\u30BB\u30C3\u30C8\uFF09\u306F\u305A\u3067\u3059\u304C\u3001\u4E0B\u8A18\u304C\u5B58\u5728\u3057\u307E\u3059\uFF1A{0}
-SleuthkitCase.findFiles3.exception.msg3.text=\u30D5\u30A1\u30A4\u30EB\u540D\u306B\u57FA\u3065\u3044\u305F\u30C7\u30FC\u30BF\u30BD\u30FC\u30B9\u306E\u691C\u7D22\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002
-SleuthkitCase.addDerivedFile.exception.msg1.text=\u6D3E\u751F\u30D5\u30A1\u30A4\u30EB\u306E\u4F5C\u6210\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u306E\u65B0\u898FID\u3092\u53D6\u5F97\u3067\u304D\u307E\u305B\u3093\u3002\u30D5\u30A1\u30A4\u30EB\u540D\uFF1A{0}
-SleuthkitCase.addDerivedFile.exception.msg2.text=\u6D3E\u751F\u30D5\u30A1\u30A4\u30EB\u306E\u4F5C\u6210\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002\u30D5\u30A1\u30A4\u30EB\u540D\uFF1A{0}
-SleuthkitCase.addLocalFile.exception.msg1.text=\u30ED\u30FC\u30AB\u30EB\u30D5\u30A1\u30A4\u30EB{0}\u306E\u8FFD\u52A0\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002\u8FFD\u52A0\u5148\u306E\u30DA\u30A2\u30EC\u30F3\u30C8\u304C\u30CC\u30EB\u3067\u3059\u3002
-SleuthkitCase.addLocalFile.exception.msg2.text=\u30ED\u30FC\u30AB\u30EB\u30D5\u30A1\u30A4\u30EB\u306E\u4F5C\u6210\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u306E\u65B0\u898FID\u3092\u53D6\u5F97\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u30D5\u30A1\u30A4\u30EB\u540D\uFF1A{0}
-SleuthkitCase.addLocalFile.exception.msg3.text=\u6D3E\u751F\u30D5\u30A1\u30A4\u30EB\u306E\u4F5C\u6210\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002\u30D5\u30A1\u30A4\u30EB\u540D\uFF1A{0}
-SleuthkitCase.getLastObjectId.exception.msg.text=\u6700\u5F8C\u306E\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8ID\u3092\u53D6\u5F97\u3057\u305F\u5F8C\u3001\u7D50\u679C\u30BB\u30C3\u30C8\u3092\u9589\u3058\u308B\u969B\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002
-TskData.tskFsNameFlagEnum.allocated=\u5272\u308A\u5F53\u3066\u6E08\u307F
-TskData.tskFsNameFlagEnum.unallocated=\u672A\u5272\u308A\u5F53\u3066
-TskData.tskFsMetaFlagEnum.allocated=\u5272\u308A\u5F53\u3066\u6E08\u307F
-TskData.tskFsMetaFlagEnum.unallocated=\u672A\u5272\u308A\u5F53\u3066
-TskData.tskFsMetaFlagEnum.used=\u4F7F\u7528\u6E08\u307F
-TskData.tskFsMetaFlagEnum.unused=\u672A\u4F7F\u7528
-TskData.tskFsMetaFlagEnum.compressed=\u5727\u7E2E\u6E08\u307F
-TskData.tskFsMetaFlagEnum.orphan=\u30AA\u30FC\u30D5\u30A1\u30F3
-TskData.tskImgTypeEnum.autoDetect=\u81EA\u52D5\u691C\u51FA
-TskData.tskImgTypeEnum.rawSingle=\u30ED\u30FC\u30B7\u30F3\u30B0\u30EB
-TskData.tskImgTypeEnum.rawSplit=\u30ED\u30FC\u30B9\u30D7\u30EA\u30C3\u30C8
-TskData.tskImgTypeEnum.unknown=\u4E0D\u660E
-TskData.tskVSTypeEnum.autoDetect=\u81EA\u52D5\u691C\u51FA
-TskData.tskVSTypeEnum.fake=\u507D\u7269
-TskData.tskVSTypeEnum.unsupported=\u975E\u30B5\u30DD\u30FC\u30C8
-TskData.fileKnown.unknown=\u4E0D\u660E
-TskData.fileKnown.known=\u65E2\u77E5
-TskData.fileKnown.knownBad=\u65E2\u77E5\u60AA\u8CEA
-TskData.objectTypeEnum.exception.msg1.text=\u30D0\u30EA\u30E5\u30FC\uFF1A{0}\u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u30BF\u30A4\u30D7\u306B\u8A72\u5F53\u3057\u307E\u305B\u3093
-Volume.desc.text=\u4E0D\u660E
-Volume.read.exception.msg1.text=\u3053\u306E\u30DC\u30EA\u30E5\u30FC\u30E0\u306E\u30DA\u30A2\u30EC\u30F3\u30C8\u306FVolmueSystem\u3067\u3042\u308B\u3079\u304D\u3067\u3059\u304C\u3001\u9055\u3044\u307E\u3059\u3002
-Volume.vsFlagToString.allocated=\u5272\u308A\u5F53\u3066\u6E08\u307F
-Volume.vsFlagToString.unallocated=\u672A\u5272\u308A\u5F53\u3066
-TskData.fileKnown.exception.msg1.text=\u30D0\u30EA\u30E5\u30FC\uFF1A{0}\u306FFileKnown\u306B\u8A72\u5F53\u3057\u307E\u305B\u3093
-TskData.tskDbFilesTypeEnum.exception.msg1.text=\u30D0\u30EA\u30E5\u30FC\uFF1A{0}\u306FTSK_FILE_TYPE_ENUM\u306B\u8A72\u5F53\u3057\u307E\u305B\u3093
-TskData.tskVSTypeEnum.exception.msg1.text=\u30D0\u30EA\u30E5\u30FC\uFF1A{0}\u306FTSK_VS_TYPE_ENUM\u306B\u8A72\u5F53\u3057\u307E\u305B\u3093
-TskData.tskImgTypeEnum.exception.msg1.text=\u30D0\u30EA\u30E5\u30FC\uFF1A{0}\u306FTSK_IMG_TYPE_ENUM\u306B\u8A72\u5F53\u3057\u307E\u305B\u3093
-TskData.tskFsTypeEnum.exception.msg1.text=\u30D0\u30EA\u30E5\u30FC\uFF1A{0}\u306FTSK_FS_TYPE_ENUM\u306B\u8A72\u5F53\u3057\u307E\u305B\u3093
-TskData.tskFsAttrTypeEnum.exception.msg1.text=\u30D0\u30EA\u30E5\u30FC\uFF1A{0}\u306FTSK_FS_TYPE_ENUM\u306B\u8A72\u5F53\u3057\u307E\u305B\u3093
-TskData.tskFsNameFlagEnum.exception.msg1.text=\u30D0\u30EA\u30E5\u30FC\uFF1A{0}\u306FTSK_FS_NAME_FLAG_ENUM\u306B\u8A72\u5F53\u3057\u307E\u305B\u3093
-TskData.tskFsMetaTypeEnum.exception.msg1.text=\u30D0\u30EA\u30E5\u30FC\uFF1A{0}\u306FTSK_FS_META_TYPE_ENUM\u306B\u8A72\u5F53\u3057\u307E\u305B\u3093
-TskData.tskFsNameTypeEnum.exception.msg1.text=\u30D0\u30EA\u30E5\u30FC\uFF1A{0}\u306FTSK_FS_NAME_TYPE_ENUM\u306B\u8A72\u5F53\u3057\u307E\u305B\u3093
-SleuthkitCase.findFiles.exception.msg1.text=\u30A8\u30E9\u30FC\uFF1A\u30C7\u30FC\u30BF\u30BD\u30FC\u30B9\u306F\u30DA\u30A2\u30EC\u30F3\u30C8\u304C\u7121\u3044\uFF08\u30A4\u30E1\u30FC\u30B8\u3001\u30D5\u30A1\u30A4\u30EB\u30BB\u30C3\u30C8\uFF09\u306F\u305A\u3067\u3059\u304C\u3001\u4E0B\u8A18\u304C\u5B58\u5728\u3057\u307E\u3059\uFF1A{0}
-SleuthkitCase.isFileFromSource.exception.msg2.text=\u30A8\u30E9\u30FC\uFF1A\u30C7\u30FC\u30BF\u30BD\u30FC\u30B9\u306F\u30A4\u30E1\u30FC\u30B8\u307E\u305F\u306FVirtualDirectory\u3067\u3042\u308B\u3079\u304D\u3067\u3059\u304C\u3001\u4E0B\u8A18\u304C\u5B58\u5728\u3057\u307E\u3059\uFF1A{0}
-SleuthkitCase.findFiles.exception.msg2.text=\u30A8\u30E9\u30FC\uFF1A\u30C7\u30FC\u30BF\u30BD\u30FC\u30B9\u306F\u30A4\u30E1\u30FC\u30B8\u307E\u305F\u306FVirtualDirectory\u3067\u3042\u308B\u3079\u304D\u3067\u3059\u304C\u3001\u4E0B\u8A18\u304C\u5B58\u5728\u3057\u307E\u3059\uFF1A{0}
-SleuthkitCase.findFiles3.exception.msg2.text=\u30A8\u30E9\u30FC\uFF1A\u30C7\u30FC\u30BF\u30BD\u30FC\u30B9\u306F\u30A4\u30E1\u30FC\u30B8\u307E\u305F\u306FVirtualDirectory\u3067\u3042\u308B\u3079\u304D\u3067\u3059\u304C\u3001\u4E0B\u8A18\u304C\u5B58\u5728\u3057\u307E\u3059\uFF1A{0}
-DerviedFile.derivedMethod.exception.msg1.text=\u30D5\u30A1\u30A4\u30EBID\uFF1A{0}\u306E\u6D3E\u751F\u65B9\u6CD5\u3092\u53D6\u5F97\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F
-BlackboardArtifact.tskGpsRoute.text=GPS\u30EB\u30FC\u30C8
-BlackboardAttribute.tskGeoLatitudeStart.text=\u30B9\u30BF\u30FC\u30C8\u7DEF\u5EA6
-BlackboardAttribute.tskGeoLatitudeEnd.text=\u7D42\u4E86\u7DEF\u5EA6
-BlackboardAttribute.tskGeoLongitudeStart.text=\u30B9\u30BF\u30FC\u30C8\u7D4C\u5EA6
-BlackboardAttribute.tskGeoLongitudeEnd.text=\u7D42\u4E86\u7D4C\u5EA6
-BlackboardAttribute.tskReadStatus.text=\u8AAD\u3080
\ No newline at end of file
+BlackboardAttribute.tskEmailFrom.text=\u9001\u4fe1\u5143E\u30e1\u30fc\u30eb
+BlackboardAttribute.tskEmailContentPlain.text=\u30e1\u30c3\u30bb\u30fc\u30b8\uff08\u30d7\u30ec\u30fc\u30f3\u30c6\u30ad\u30b9\u30c8\uff09
+BlackboardAttribute.tskEmailContentHtml.text=\u30e1\u30c3\u30bb\u30fc\u30b8\uff08HTML\uff09
+BlackboardAttribute.tskEmailContentRtf.text=\u30e1\u30c3\u30bb\u30fc\u30b8\uff08RTF\uff09
+BlackboardAttribute.tskMsgId.text=\u30e1\u30c3\u30bb\u30fc\u30b8ID
+BlackboardAttribute.tskMsgReplyId.text=\u30e1\u30c3\u30bb\u30fc\u30b8\u30ea\u30d7\u30e9\u30a4ID
+BlackboardAttribute.tskDateTimeRcvd.text=\u53d7\u4fe1\u65e5
+BlackboardAttribute.tskDateTimeSent.text=\u9001\u4fe1\u65e5
+BlackboardAttribute.tskSubject.text=\u30b5\u30d6\u30b8\u30a7\u30af\u30c8
+BlackboardAttribute.tskTitle.text=\u30bf\u30a4\u30c8\u30eb
+BlackboardAttribute.tskGeoLatitude.text=\u7def\u5ea6
+BlackboardAttribute.tskGeoLongitude.text=\u7d4c\u5ea6
+BlackboardAttribute.tskGeoVelocity.text=\u901f\u5ea6
+BlackboardAttribute.tskGeoAltitude.text=\u6a19\u9ad8
+BlackboardAttribute.tskGeoBearing.text=\u65b9\u5411
+BlackboardAttribute.tskGeoHPrecision.text=\u6c34\u5e73\u7cbe\u5ea6
+BlackboardAttribute.tskGeoVPrecision.text=\u5782\u76f4\u7cbe\u5ea6
+BlackboardAttribute.tskGeoMapDatum.text=\u6e2c\u5730\u7cfb
+BlackboardAttribute.tskFileTypeSig.text=\u30d5\u30a1\u30a4\u30eb\u30bf\u30a4\u30d7\uff08\u30b7\u30b0\u30cd\u30c1\u30e3\uff09
+BlackboardAttribute.tskFileTypeExt.text=\u30d5\u30a1\u30a4\u30eb\u30bf\u30a4\u30d7\uff08\u62e1\u5f35\u5b50\uff09
+BlackboardAttribute.tskTaggedArtifact.text=\u30bf\u30b0\u4ed8\u3051\u3055\u308c\u305f\u7d50\u679c
+BlackboardAttribute.tskTagName.text=\u30bf\u30b0\u540d
+BlackboardAttribute.tskComment.text=\u30b3\u30e1\u30f3\u30c8
+BlackboardAttribute.tskUrlDecoded.text=\u5fa9\u53f7\u5316\u3055\u308c\u305fURL
+BlackboardAttribute.tskDateTimeCreated.text=\u4f5c\u6210\u65e5
+BlackboardAttribute.tskDateTimeModified.text=\u4fee\u6b63\u65e5
+BlackboardAttribute.tskProcessorArchitecture.text=\u30d7\u30ed\u30bb\u30c3\u30b5\u30a2\u30fc\u30ad\u30c6\u30af\u30c1\u30e3
+BlackboardAttribute.tskVersion.text=\u30d0\u30fc\u30b8\u30e7\u30f3
+BlackboardAttribute.tskUserId.text=\u30e6\u30fc\u30b6ID
+BlackboardAttribute.tskDescription.text=\u8aac\u660e
+BlackboardAttribute.tskMessageType.text=\u30e1\u30c3\u30bb\u30fc\u30b8\u30bf\u30a4\u30d7
+BlackboardAttribute.tskPhoneNumberHome.text=\u96fb\u8a71\u756a\u53f7\uff08\u81ea\u5b85\uff09
+BlackboardAttribute.tskPhoneNumberOffice.text=\u96fb\u8a71\u756a\u53f7\uff08\u4f1a\u793e\uff09
+BlackboardAttribute.tskPhoneNumberMobile.text=\u96fb\u8a71\u756a\u53f7\uff08\u643a\u5e2f\uff09
+BlackboardAttribute.tskPhoneNumberFrom.text=\u767a\u4fe1\u8005\u96fb\u8a71\u756a\u53f7
+BlackboardAttribute.tskPhoneNumberTo.text=\u7740\u4fe1\u8005\u96fb\u8a71\u756a\u53f7
+BlackboardAttribute.tskDirection.text=\u65b9\u5411
+BlackboardAttribute.tskEmailHome.text=E\u30e1\u30fc\u30eb\uff08\u81ea\u5b85\uff09
+BlackboardAttribute.tskEmailOffice.text=E\u30e1\u30fc\u30eb\uff08\u30aa\u30d5\u30a3\u30b9\uff09
+BlackboardAttribute.tskDateTimeStart.text=\u958b\u59cb\u65e5\u4ed8\uff0f\u6642\u523b
+BlackboardAttribute.tskDateTimeEnd.text=\u7d42\u4e86\u65e5\u4ed8\uff0f\u6642\u523b
+BlackboardAttribute.tskCalendarEntryType.text=\u30ab\u30ec\u30f3\u30c0\u30fc\u30a8\u30f3\u30c8\u30ea\u30fc\u30bf\u30a4\u30d7
+BlackboardAttribute.tskLocation.text=\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3
+BlackboardAttribute.tskShortcut.text=\u30b7\u30e7\u30fc\u30c8\u30ab\u30c3\u30c8
+BlackboardAttribute.tskDeviceName.text=\u6a5f\u5668\u540d
+BlackboardAttribute.tskCategory.text=\u30ab\u30c6\u30b4\u30ea\u30fc
+BlackboardAttribute.tskEmailReplyTo.text=\u8fd4\u4fe1\u30a2\u30c9\u30ec\u30b9
+BlackboardAttribute.tskServerName.text=\u30b5\u30fc\u30d0\u540d
+BlackboardAttribute.tskCount.text=\u30ab\u30a6\u30f3\u30c8
+BlackboardAttribute.tskMinCount.text=\u6700\u5c0f\u30ab\u30a6\u30f3\u30c8
+BlackboardAttribute.tskPathSource.text=\u30d1\u30b9\u30bd\u30fc\u30b9
+BlackboardAttribute.tskPermissions.text=\u30d1\u30fc\u30df\u30c3\u30b7\u30e7\u30f3
+BlackboardAttribute.tskAssociatedArtifact.text=\u95a2\u9023\u3059\u308b\u30a2\u30fc\u30c6\u30a3\u30d5\u30a1\u30af\u30c8
+BlackboardAttribute.tskIsDeleted.text=\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f
+AbstractFile.readLocal.exception.msg4.text=\u30d5\u30a1\u30a4\u30eb{0}\u306e\u8aad\u307f\u53d6\u308a\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f
+AbstractFile.readLocal.exception.msg1.text=\u30ed\u30fc\u30ab\u30eb\u30d5\u30a1\u30a4\u30eb\u306e\u8aad\u307f\u53d6\u308a\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u30ed\u30fc\u30ab\u30eb\u30d1\u30b9\u304c\u30bb\u30c3\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002
+AbstractFile.readLocal.exception.msg2.text=\u30ed\u30fc\u30ab\u30eb\u30d5\u30a1\u30a4\u30eb\u306e\u8aad\u307f\u53d6\u308a\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u4e0b\u8a18\u306e\u30ed\u30fc\u30ab\u30eb\u30d1\u30b9\u306b\u306f\u5b58\u5728\u3057\u307e\u305b\u3093\uff1a{0}
+AbstractFile.readLocal.exception.msg3.text=\u30ed\u30fc\u30ab\u30eb\u30d5\u30a1\u30a4\u30eb\u306e\u8aad\u307f\u53d6\u308a\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u4e0b\u8a18\u306e\u30ed\u30fc\u30ab\u30eb\u30d1\u30b9\u3067\u306f\u8aad\u307f\u53d6\u308a\u3067\u304d\u307e\u305b\u3093\uff1a{0}
+AbstractFile.readLocal.exception.msg5.text=\u30ed\u30fc\u30ab\u30eb\u30d5\u30a1\u30a4\u30eb{0}\u3092\u8aad\u307f\u53d6\u308c\u307e\u305b\u3093
+FsContent.readInt.err.context.text=\u30a4\u30e1\u30fc\u30b8\u30d5\u30a1\u30a4\u30eb\u306e\u8aad\u307f\u53d6\u308a\u30a8\u30e9\u30fc
+FsContent.readInt.err.msg.text=\u30a4\u30e1\u30fc\u30b8\u30d5\u30a1\u30a4\u30eb\u306f\u5b58\u5728\u3057\u306a\u3044\u304b\u3001\u30a2\u30af\u30bb\u30b9\u304c\u62d2\u5426\u3055\u308c\u307e\u3057\u305f\u3002
+Image.verifyImageSize.errStr1.text=\n\u4e0d\u5b8c\u5168\u306a\u30a4\u30e1\u30fc\u30b8\u306e\u53ef\u80fd\u6027\uff1a\u30aa\u30d5\u30bb\u30c3\u30c8{0}\u306b\u57fa\u3065\u3044\u305f\u30dc\u30ea\u30e5\u30fc\u30e0\u306e\u8aad\u307f\u53d6\u308a\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f
+Image.verifyImageSize.errStr2.text=\n\u4e0d\u5b8c\u5168\u306a\u30a4\u30e1\u30fc\u30b8\u306e\u53ef\u80fd\u6027\uff1a\u30aa\u30d5\u30bb\u30c3\u30c8{0}\u306b\u57fa\u3065\u3044\u305f\u30dc\u30ea\u30e5\u30fc\u30e0\u306e\u8aad\u307f\u53d6\u308a\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f
+Image.verifyImageSize.errStr3.text=\n\u4e0d\u5b8c\u5168\u306a\u30a4\u30e1\u30fc\u30b8\u306e\u53ef\u80fd\u6027\uff1a\u30aa\u30d5\u30bb\u30c3\u30c8{0}\u306b\u57fa\u3065\u3044\u305f\u30d5\u30a1\u30a4\u30eb\u30b7\u30b9\u30c6\u30e0\u306e\u8aad\u307f\u53d6\u308a\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f
+Image.verifyImageSize.errStr4.text=\n\u4e0d\u5b8c\u5168\u306a\u30a4\u30e1\u30fc\u30b8\u306e\u53ef\u80fd\u6027\uff1a\u30aa\u30d5\u30bb\u30c3\u30c8{0}\u306b\u57fa\u3065\u3044\u305f\u30d5\u30a1\u30a4\u30eb\u30b7\u30b9\u30c6\u30e0\u306e\u8aad\u307f\u53d6\u308a\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f
+SleuthkitCase.isFileFromSource.exception.msg.text=\u30a8\u30e9\u30fc\uff1a\u30c7\u30fc\u30bf\u30bd\u30fc\u30b9\u306f\u30da\u30a2\u30ec\u30f3\u30c8\u304c\u7121\u3044\uff08\u30a4\u30e1\u30fc\u30b8\u3001\u30d5\u30a1\u30a4\u30eb\u30bb\u30c3\u30c8\uff09\u306f\u305a\u3067\u3059\u304c\u3001\u4e0b\u8a18\u304c\u5b58\u5728\u3057\u307e\u3059\uff1a{0}
+SleuthkitCase.findFiles.exception.msg3.text=\u30d5\u30a1\u30a4\u30eb\u540d\u306b\u57fa\u3065\u3044\u305f\u30c7\u30fc\u30bf\u30bd\u30fc\u30b9\u306e\u691c\u7d22\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002
+SleuthkitCase.findFiles3.exception.msg1.text=\u30a8\u30e9\u30fc\uff1a\u30c7\u30fc\u30bf\u30bd\u30fc\u30b9\u306f\u30da\u30a2\u30ec\u30f3\u30c8\u304c\u7121\u3044\uff08\u30a4\u30e1\u30fc\u30b8\u3001\u30d5\u30a1\u30a4\u30eb\u30bb\u30c3\u30c8\uff09\u306f\u305a\u3067\u3059\u304c\u3001\u4e0b\u8a18\u304c\u5b58\u5728\u3057\u307e\u3059\uff1a{0}
+SleuthkitCase.findFiles3.exception.msg3.text=\u30d5\u30a1\u30a4\u30eb\u540d\u306b\u57fa\u3065\u3044\u305f\u30c7\u30fc\u30bf\u30bd\u30fc\u30b9\u306e\u691c\u7d22\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002
+SleuthkitCase.addDerivedFile.exception.msg1.text=\u6d3e\u751f\u30d5\u30a1\u30a4\u30eb\u306e\u4f5c\u6210\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u30aa\u30d6\u30b8\u30a7\u30af\u30c8\u306e\u65b0\u898fID\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3002\u30d5\u30a1\u30a4\u30eb\u540d\uff1a{0}
+SleuthkitCase.addDerivedFile.exception.msg2.text=\u6d3e\u751f\u30d5\u30a1\u30a4\u30eb\u306e\u4f5c\u6210\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u30d5\u30a1\u30a4\u30eb\u540d\uff1a{0}
+SleuthkitCase.addLocalFile.exception.msg1.text=\u30ed\u30fc\u30ab\u30eb\u30d5\u30a1\u30a4\u30eb{0}\u306e\u8ffd\u52a0\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u8ffd\u52a0\u5148\u306e\u30da\u30a2\u30ec\u30f3\u30c8\u304c\u30cc\u30eb\u3067\u3059\u3002
+SleuthkitCase.addLocalFile.exception.msg2.text=\u30ed\u30fc\u30ab\u30eb\u30d5\u30a1\u30a4\u30eb\u306e\u4f5c\u6210\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u30aa\u30d6\u30b8\u30a7\u30af\u30c8\u306e\u65b0\u898fID\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30d5\u30a1\u30a4\u30eb\u540d\uff1a{0}
+SleuthkitCase.addLocalFile.exception.msg3.text=\u6d3e\u751f\u30d5\u30a1\u30a4\u30eb\u306e\u4f5c\u6210\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u30d5\u30a1\u30a4\u30eb\u540d\uff1a{0}
+SleuthkitCase.getLastObjectId.exception.msg.text=\u6700\u5f8c\u306e\u30aa\u30d6\u30b8\u30a7\u30af\u30c8ID\u3092\u53d6\u5f97\u3057\u305f\u5f8c\u3001\u7d50\u679c\u30bb\u30c3\u30c8\u3092\u9589\u3058\u308b\u969b\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002
+TskData.tskFsNameFlagEnum.allocated=\u5272\u308a\u5f53\u3066\u6e08\u307f
+TskData.tskFsNameFlagEnum.unallocated=\u672a\u5272\u308a\u5f53\u3066
+TskData.tskFsMetaFlagEnum.allocated=\u5272\u308a\u5f53\u3066\u6e08\u307f
+TskData.tskFsMetaFlagEnum.unallocated=\u672a\u5272\u308a\u5f53\u3066
+TskData.tskFsMetaFlagEnum.used=\u4f7f\u7528\u6e08\u307f
+TskData.tskFsMetaFlagEnum.unused=\u672a\u4f7f\u7528
+TskData.tskFsMetaFlagEnum.compressed=\u5727\u7e2e\u6e08\u307f
+TskData.tskFsMetaFlagEnum.orphan=\u30aa\u30fc\u30d5\u30a1\u30f3
+TskData.tskImgTypeEnum.autoDetect=\u81ea\u52d5\u691c\u51fa
+TskData.tskImgTypeEnum.rawSingle=\u30ed\u30fc\u30b7\u30f3\u30b0\u30eb
+TskData.tskImgTypeEnum.rawSplit=\u30ed\u30fc\u30b9\u30d7\u30ea\u30c3\u30c8
+TskData.tskImgTypeEnum.unknown=\u4e0d\u660e
+TskData.tskVSTypeEnum.autoDetect=\u81ea\u52d5\u691c\u51fa
+TskData.tskVSTypeEnum.fake=\u507d\u7269
+TskData.tskVSTypeEnum.unsupported=\u975e\u30b5\u30dd\u30fc\u30c8
+TskData.fileKnown.unknown=\u4e0d\u660e
+TskData.fileKnown.known=\u65e2\u77e5
+TskData.fileKnown.knownBad=\u65e2\u77e5\u60aa\u8cea
+TskData.objectTypeEnum.exception.msg1.text=\u30d0\u30ea\u30e5\u30fc\uff1a{0}\u306f\u30aa\u30d6\u30b8\u30a7\u30af\u30c8\u30bf\u30a4\u30d7\u306b\u8a72\u5f53\u3057\u307e\u305b\u3093
+Volume.desc.text=\u4e0d\u660e
+Volume.read.exception.msg1.text=\u3053\u306e\u30dc\u30ea\u30e5\u30fc\u30e0\u306e\u30da\u30a2\u30ec\u30f3\u30c8\u306fVolmueSystem\u3067\u3042\u308b\u3079\u304d\u3067\u3059\u304c\u3001\u9055\u3044\u307e\u3059\u3002
+Volume.vsFlagToString.allocated=\u5272\u308a\u5f53\u3066\u6e08\u307f
+Volume.vsFlagToString.unallocated=\u672a\u5272\u308a\u5f53\u3066
+TskData.fileKnown.exception.msg1.text=\u30d0\u30ea\u30e5\u30fc\uff1a{0}\u306fFileKnown\u306b\u8a72\u5f53\u3057\u307e\u305b\u3093
+TskData.tskDbFilesTypeEnum.exception.msg1.text=\u30d0\u30ea\u30e5\u30fc\uff1a{0}\u306fTSK_FILE_TYPE_ENUM\u306b\u8a72\u5f53\u3057\u307e\u305b\u3093
+TskData.tskVSTypeEnum.exception.msg1.text=\u30d0\u30ea\u30e5\u30fc\uff1a{0}\u306fTSK_VS_TYPE_ENUM\u306b\u8a72\u5f53\u3057\u307e\u305b\u3093
+TskData.tskImgTypeEnum.exception.msg1.text=\u30d0\u30ea\u30e5\u30fc\uff1a{0}\u306fTSK_IMG_TYPE_ENUM\u306b\u8a72\u5f53\u3057\u307e\u305b\u3093
+TskData.tskFsTypeEnum.exception.msg1.text=\u30d0\u30ea\u30e5\u30fc\uff1a{0}\u306fTSK_FS_TYPE_ENUM\u306b\u8a72\u5f53\u3057\u307e\u305b\u3093
+TskData.tskFsAttrTypeEnum.exception.msg1.text=\u30d0\u30ea\u30e5\u30fc\uff1a{0}\u306fTSK_FS_TYPE_ENUM\u306b\u8a72\u5f53\u3057\u307e\u305b\u3093
+TskData.tskFsNameFlagEnum.exception.msg1.text=\u30d0\u30ea\u30e5\u30fc\uff1a{0}\u306fTSK_FS_NAME_FLAG_ENUM\u306b\u8a72\u5f53\u3057\u307e\u305b\u3093
+TskData.tskFsMetaTypeEnum.exception.msg1.text=\u30d0\u30ea\u30e5\u30fc\uff1a{0}\u306fTSK_FS_META_TYPE_ENUM\u306b\u8a72\u5f53\u3057\u307e\u305b\u3093
+TskData.tskFsNameTypeEnum.exception.msg1.text=\u30d0\u30ea\u30e5\u30fc\uff1a{0}\u306fTSK_FS_NAME_TYPE_ENUM\u306b\u8a72\u5f53\u3057\u307e\u305b\u3093
+SleuthkitCase.findFiles.exception.msg1.text=\u30a8\u30e9\u30fc\uff1a\u30c7\u30fc\u30bf\u30bd\u30fc\u30b9\u306f\u30da\u30a2\u30ec\u30f3\u30c8\u304c\u7121\u3044\uff08\u30a4\u30e1\u30fc\u30b8\u3001\u30d5\u30a1\u30a4\u30eb\u30bb\u30c3\u30c8\uff09\u306f\u305a\u3067\u3059\u304c\u3001\u4e0b\u8a18\u304c\u5b58\u5728\u3057\u307e\u3059\uff1a{0}
+SleuthkitCase.isFileFromSource.exception.msg2.text=\u30a8\u30e9\u30fc\uff1a\u30c7\u30fc\u30bf\u30bd\u30fc\u30b9\u306f\u30a4\u30e1\u30fc\u30b8\u307e\u305f\u306fVirtualDirectory\u3067\u3042\u308b\u3079\u304d\u3067\u3059\u304c\u3001\u4e0b\u8a18\u304c\u5b58\u5728\u3057\u307e\u3059\uff1a{0}
+SleuthkitCase.findFiles.exception.msg2.text=\u30a8\u30e9\u30fc\uff1a\u30c7\u30fc\u30bf\u30bd\u30fc\u30b9\u306f\u30a4\u30e1\u30fc\u30b8\u307e\u305f\u306fVirtualDirectory\u3067\u3042\u308b\u3079\u304d\u3067\u3059\u304c\u3001\u4e0b\u8a18\u304c\u5b58\u5728\u3057\u307e\u3059\uff1a{0}
+SleuthkitCase.findFiles3.exception.msg2.text=\u30a8\u30e9\u30fc\uff1a\u30c7\u30fc\u30bf\u30bd\u30fc\u30b9\u306f\u30a4\u30e1\u30fc\u30b8\u307e\u305f\u306fVirtualDirectory\u3067\u3042\u308b\u3079\u304d\u3067\u3059\u304c\u3001\u4e0b\u8a18\u304c\u5b58\u5728\u3057\u307e\u3059\uff1a{0}
+DerviedFile.derivedMethod.exception.msg1.text=\u30d5\u30a1\u30a4\u30ebID\uff1a{0}\u306e\u6d3e\u751f\u65b9\u6cd5\u3092\u53d6\u5f97\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f
+BlackboardArtifact.tskGpsRoute.text=GPS\u30eb\u30fc\u30c8
+BlackboardAttribute.tskGeoLatitudeStart.text=\u30b9\u30bf\u30fc\u30c8\u7def\u5ea6
+BlackboardAttribute.tskGeoLatitudeEnd.text=\u7d42\u4e86\u7def\u5ea6
+BlackboardAttribute.tskGeoLongitudeStart.text=\u30b9\u30bf\u30fc\u30c8\u7d4c\u5ea6
+BlackboardAttribute.tskGeoLongitudeEnd.text=\u7d42\u4e86\u7d4c\u5ea6
+BlackboardAttribute.tskReadStatus.text=\u8aad\u3080
diff --git a/bindings/java/src/org/sleuthkit/datamodel/EventDB.java b/bindings/java/src/org/sleuthkit/datamodel/EventDB.java
new file mode 100644
index 0000000000000000000000000000000000000000..121091c0e0f56ab5ffdff7497c65f2f0550702d6
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/EventDB.java
@@ -0,0 +1,1720 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013-18 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.datamodel;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.SetMultimap;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.Types;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Function;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.tuple.ImmutablePair;
+import org.joda.time.DateTimeZone;
+import org.joda.time.Interval;
+import org.joda.time.Period;
+import static org.sleuthkit.datamodel.SleuthkitCase.closeResultSet;
+import static org.sleuthkit.datamodel.SleuthkitCase.closeStatement;
+import org.sleuthkit.datamodel.timeline.BaseTypes;
+import org.sleuthkit.datamodel.timeline.CombinedEvent;
+import org.sleuthkit.datamodel.timeline.DescriptionLoD;
+import org.sleuthkit.datamodel.timeline.EventCluster;
+import org.sleuthkit.datamodel.timeline.EventStripe;
+import org.sleuthkit.datamodel.timeline.EventType;
+import org.sleuthkit.datamodel.timeline.EventTypeZoomLevel;
+import org.sleuthkit.datamodel.timeline.RangeDivisionInfo;
+import org.sleuthkit.datamodel.timeline.RootEventType;
+import org.sleuthkit.datamodel.timeline.SingleEvent;
+import org.sleuthkit.datamodel.timeline.TimeUnits;
+import org.sleuthkit.datamodel.timeline.ZoomParams;
+import org.sleuthkit.datamodel.timeline.filters.AbstractFilter;
+import org.sleuthkit.datamodel.timeline.filters.DataSourceFilter;
+import org.sleuthkit.datamodel.timeline.filters.DataSourcesFilter;
+import org.sleuthkit.datamodel.timeline.filters.DescriptionFilter;
+import org.sleuthkit.datamodel.timeline.filters.Filter;
+import org.sleuthkit.datamodel.timeline.filters.HashHitsFilter;
+import org.sleuthkit.datamodel.timeline.filters.HashSetFilter;
+import org.sleuthkit.datamodel.timeline.filters.HideKnownFilter;
+import org.sleuthkit.datamodel.timeline.filters.IntersectionFilter;
+import org.sleuthkit.datamodel.timeline.filters.RootFilter;
+import org.sleuthkit.datamodel.timeline.filters.TagNameFilter;
+import org.sleuthkit.datamodel.timeline.filters.TagsFilter;
+import org.sleuthkit.datamodel.timeline.filters.TextFilter;
+import org.sleuthkit.datamodel.timeline.filters.TypeFilter;
+import org.sleuthkit.datamodel.timeline.filters.UnionFilter;
+import org.sqlite.SQLiteJDBCLoader;
+
+/**
+ * Provides access to the Timeline SQLite database.
+ *
+ * This class borrows a lot of ideas and techniques from SleuthkitCase. Creating
+ * an abstract base class for SQLite databases, or using a higherlevel
+ * persistence api may make sense in the future.
+ */
+public class EventDB {
+
+	private static final java.util.logging.Logger LOGGER = Logger.getLogger(EventDB.class.getName());
+
+	static {
+		//make sure sqlite driver is loaded, possibly redundant
+		try {
+			Class.forName("org.sqlite.JDBC"); // NON-NLS
+		} catch (ClassNotFoundException ex) {
+			LOGGER.log(Level.SEVERE, "Failed to load sqlite JDBC driver", ex); // NON-NLS
+		}
+	}
+
+	/**
+	 * public factory method. Creates and opens a connection to a database at
+	 * the given path. If a database does not already exist at that path, one is
+	 * created.
+	 *
+	 * @param autoCase the Autopsy Case the is events database is for.
+	 *
+	 * @return a new EventDB or null if there was an error.
+	 */
+	public static EventDB getEventDB(SleuthkitCase autoCase) {
+		try {
+			return new EventDB(autoCase);
+		} catch (Exception ex) {
+			LOGGER.log(Level.SEVERE, "error creating database connection", ex); // NON-NLS
+			return null;
+		}
+		// NON-NLS
+
+	}
+
+	private volatile SleuthkitCase.CaseDbConnection con;
+
+	private PreparedStatement getEventByIDStmt;
+	private PreparedStatement getMaxTimeStmt;
+	private PreparedStatement getMinTimeStmt;
+	private PreparedStatement getDataSourceIDsStmt;
+	private PreparedStatement getHashSetNamesStmt;
+	private PreparedStatement insertRowStmt;
+	private PreparedStatement insertHashSetStmt;
+	private PreparedStatement insertHashHitStmt;
+	private PreparedStatement insertTagStmt;
+	private PreparedStatement deleteTagStmt;
+	private PreparedStatement selectHashSetStmt;
+	private PreparedStatement countAllEventsStmt;
+	private PreparedStatement dropEventsTableStmt;
+	private PreparedStatement dropHashSetHitsTableStmt;
+	private PreparedStatement dropHashSetsTableStmt;
+	private PreparedStatement dropTagsTableStmt;
+	private PreparedStatement dropDBInfoTableStmt;
+	private PreparedStatement selectNonArtifactEventIDsByObjectIDStmt;
+	private PreparedStatement selectEventIDsBYObjectAndArtifactIDStmt;
+
+	private final Set<PreparedStatement> preparedStatements = new HashSet<PreparedStatement>();
+
+	private final Lock DBLock = new ReentrantReadWriteLock(true).writeLock(); //using exclusive lock for all db ops for now
+	private final SleuthkitCase sleuthkitCase;
+
+	private EventDB(SleuthkitCase tskCase) throws SQLException, Exception {
+		//should this go into module output (or even cache, we should be able to rebuild it)?
+		sleuthkitCase = tskCase;
+		initializeDB();
+	}
+
+	@Override
+	public void finalize() throws Throwable {
+		try {
+			closeDBCon();
+		} finally {
+			super.finalize();
+		}
+	}
+
+	void closeDBCon() {
+		if (con != null) {
+			try {
+				closeStatements();
+				con.close();
+			} catch (SQLException ex) {
+				LOGGER.log(Level.WARNING, "Failed to close connection to evetns.db", ex); // NON-NLS
+			}
+		}
+		con = null;
+	}
+
+	public Interval getSpanningInterval(Collection<Long> eventIDs) {
+		DBLock.lock();
+		Statement stmt = null;
+		ResultSet rs = null; // NON-NLS
+		try {
+			stmt = con.createStatement();
+			rs = stmt.executeQuery("SELECT Min(time), Max(time) FROM events WHERE event_id IN (" + StringUtils.joinAsStrings(eventIDs, ", ") + ")");
+			while (rs.next()) {
+				return new Interval(rs.getLong("Min(time)") * 1000, (rs.getLong("Max(time)") + 1) * 1000, DateTimeZone.UTC); // NON-NLS
+			}
+		} catch (SQLException ex) {
+			LOGGER.log(Level.SEVERE, "Error executing get spanning interval query.", ex); // NON-NLS
+		} finally {
+			SleuthkitCase.closeStatement(stmt);
+			SleuthkitCase.closeResultSet(rs);
+			DBLock.unlock();
+		}
+		return null;
+	}
+
+	public EventTransaction beginTransaction() {
+		return new EventTransaction();
+	}
+
+	public void commitTransaction(EventTransaction tr) {
+		if (tr.isClosed()) {
+			throw new IllegalArgumentException("can't close already closed transaction"); // NON-NLS
+		}
+		tr.commit();
+	}
+
+	/**
+	 * @return the total number of events in the database or, -1 if there is an
+	 *         error.
+	 */
+	public int countAllEvents() {
+		DBLock.lock();
+		ResultSet rs = null;
+		try {
+			rs = countAllEventsStmt.executeQuery();
+
+			while (rs.next()) {
+				return rs.getInt("count"); // NON-NLS
+			}
+		} catch (SQLException ex) {
+			LOGGER.log(Level.SEVERE, "Error counting all events", ex); //NON-NLS
+		} finally {
+			SleuthkitCase.closeResultSet(rs);
+			DBLock.unlock();
+		}
+		return -1;
+	}
+
+	/**
+	 * get the count of all events that fit the given zoom params organized by
+	 * the EvenType of the level spcified in the ZoomParams
+	 *
+	 * @param params the params that control what events to count and how to
+	 *               organize the returned map
+	 *
+	 * @return a map from event type( of the requested level) to event counts
+	 */
+	public Map<EventType, Long> countEventsByType(ZoomParams params) {
+		if (params.getTimeRange() != null) {
+			return countEventsByType(params.getTimeRange().getStartMillis() / 1000,
+					params.getTimeRange().getEndMillis() / 1000,
+					params.getFilter(), params.getTypeZoomLevel());
+		} else {
+			return Collections.emptyMap();
+		}
+	}
+
+	/**
+	 * get a count of tagnames applied to the given event ids as a map from
+	 * tagname displayname to count of tag applications
+	 *
+	 * @param eventIDsWithTags the event ids to get the tag counts map for
+	 *
+	 * @return a map from tagname displayname to count of applications
+	 */
+	public Map<String, Long> getTagCountsByTagName(Set<Long> eventIDsWithTags) {
+		HashMap<String, Long> counts = new HashMap<String, Long>();
+		DBLock.lock();
+		ResultSet resultSet = null;
+		Statement statement = null;
+		try {
+
+			statement = con.createStatement();
+			resultSet = statement.executeQuery("SELECT tag_name_display_name, COUNT(DISTINCT tag_id) AS count FROM tags" //NON-NLS
+					+ " WHERE event_id IN (" + StringUtils.joinAsStrings(eventIDsWithTags, ", ") + ")" //NON-NLS
+					+ " GROUP BY tag_name_id" //NON-NLS
+					+ " ORDER BY tag_name_display_name");
+			while (resultSet.next()) {
+				counts.put(resultSet.getString("tag_name_display_name"), resultSet.getLong("count")); //NON-NLS
+			}
+		} catch (SQLException ex) {
+			LOGGER.log(Level.SEVERE, "Failed to get tag counts by tag name.", ex); //NON-NLS
+		} finally {
+			SleuthkitCase.closeResultSet(resultSet);
+			SleuthkitCase.closeStatement(statement);
+			DBLock.unlock();
+		}
+		return counts;
+	}
+
+	/**
+	 * drop the tables from this database and recreate them in order to start
+	 * over.
+	 */
+	public void reInitializeDB() throws TskCoreException {
+		DBLock.lock();
+		try {
+			dropEventsTableStmt.executeUpdate();
+			dropHashSetHitsTableStmt.executeUpdate();
+			dropHashSetsTableStmt.executeUpdate();
+			dropTagsTableStmt.executeUpdate();
+			dropDBInfoTableStmt.executeUpdate();
+			initializeDB();
+		} catch (SQLException ex) {
+			LOGGER.log(Level.SEVERE, "could not drop old tables", ex); // NON-NLS
+		} finally {
+			DBLock.unlock();
+		}
+	}
+
+	/**
+	 * drop only the tags table and rebuild it incase the tags have changed
+	 * while TL was not listening,
+	 */
+	public void reInitializeTags() {
+		DBLock.lock();
+		try {
+			dropTagsTableStmt.executeUpdate();
+			initializeTagsTable();
+		} catch (SQLException ex) {
+			LOGGER.log(Level.SEVERE, "could not drop old tags table", ex); // NON-NLS
+		} finally {
+			DBLock.unlock();
+		}
+	}
+
+	public Interval getBoundingEventsInterval(Interval timeRange, RootFilter filter, DateTimeZone tz) {
+		long start = timeRange.getStartMillis() / 1000;
+		long end = timeRange.getEndMillis() / 1000;
+		final String sqlWhere = getSQLWhere(filter);
+		DBLock.lock();
+		Statement stmt = null; //can't use prepared statement because of complex where clause
+		ResultSet rs = null;
+		try {
+			stmt = con.createStatement();
+
+			rs = stmt.executeQuery(" SELECT (SELECT Max(time) FROM events " + useHashHitTablesHelper(filter) + useTagTablesHelper(filter) + " WHERE time <=" + start + " AND " + sqlWhere + ") AS start," //NON-NLS
+					+ "(SELECT Min(time)  FROM events" + useHashHitTablesHelper(filter) + useTagTablesHelper(filter) + " WHERE time >= " + end + " AND " + sqlWhere + ") AS end");
+			while (rs.next()) {
+
+				long start2 = rs.getLong("start"); // NON-NLS
+				long end2 = rs.getLong("end"); // NON-NLS
+
+				if (end2 == 0) {
+					end2 = getMaxTime();
+				}
+				return new Interval(start2 * 1000, (end2 + 1) * 1000, tz);
+			}
+		} catch (SQLException ex) {
+			LOGGER.log(Level.SEVERE, "Failed to get MIN time.", ex); // NON-NLS
+		} finally {
+			closeResultSet(rs);
+			closeStatement(stmt);
+			DBLock.unlock();
+		}
+		return null;
+	}
+
+	public SingleEvent getEventById(Long eventID) {
+		SingleEvent result = null;
+		DBLock.lock();
+		ResultSet rs = null;
+		try {
+			getEventByIDStmt.clearParameters();
+			getEventByIDStmt.setLong(1, eventID);
+
+			rs = getEventByIDStmt.executeQuery();
+			while (rs.next()) {
+				result = constructTimeLineEvent(rs);
+				break;
+			}
+		} catch (SQLException sqlEx) {
+			LOGGER.log(Level.SEVERE, "exception while querying for event with id = " + eventID, sqlEx); // NON-NLS
+		} finally {
+			closeResultSet(rs);
+			DBLock.unlock();
+		}
+		return result;
+	}
+
+	/**
+	 * Get the IDs of all the events within the given time range that pass the
+	 * given filter.
+	 *
+	 * @param timeRange The Interval that all returned events must be within.
+	 * @param filter    The Filter that all returned events must pass.
+	 *
+	 * @return A List of event ids, sorted by timestamp of the corresponding
+	 *         event..
+	 */
+	public List<Long> getEventIDs(Interval timeRange, RootFilter filter) {
+		Long startTime = timeRange.getStartMillis() / 1000;
+		Long endTime = timeRange.getEndMillis() / 1000;
+
+		if (Objects.equals(startTime, endTime)) {
+			endTime++; //make sure end is at least 1 millisecond after start
+		}
+
+		ArrayList<Long> resultIDs = new ArrayList<Long>();
+
+		DBLock.lock();
+		final String query = "SELECT events.event_id AS event_id FROM events" + useHashHitTablesHelper(filter) + useTagTablesHelper(filter)
+				+ " WHERE time >=  " + startTime + " AND time <" + endTime + " AND " + getSQLWhere(filter) + " ORDER BY time ASC"; // NON-NLS
+		Statement stmt = null;
+		ResultSet rs = null;
+		try {
+			stmt = con.createStatement();
+			rs = stmt.executeQuery(query);
+			while (rs.next()) {
+				resultIDs.add(rs.getLong("event_id")); //NON-NLS
+			}
+
+		} catch (SQLException sqlEx) {
+			LOGGER.log(Level.SEVERE, "failed to execute query for event ids in range", sqlEx); // NON-NLS
+		} finally {
+			closeResultSet(rs);
+			closeStatement(stmt);
+			DBLock.unlock();
+		}
+
+		return resultIDs;
+	}
+
+	/**
+	 * Get a representation of all the events, within the given time range, that
+	 * pass the given filter, grouped by time and description such that file
+	 * system events for the same file, with the same timestamp, are combined
+	 * together.
+	 *
+	 * @param timeRange The Interval that all returned events must be within.
+	 * @param filter    The Filter that all returned events must pass.
+	 *
+	 * @return A List of combined events, sorted by timestamp.
+	 */
+	public List<CombinedEvent> getCombinedEvents(Interval timeRange, RootFilter filter) {
+		Long startTime = timeRange.getStartMillis() / 1000;
+		Long endTime = timeRange.getEndMillis() / 1000;
+
+		if (Objects.equals(startTime, endTime)) {
+			endTime++; //make sure end is at least 1 millisecond after start
+		}
+
+		ArrayList<CombinedEvent> results = new ArrayList<CombinedEvent>();
+
+		DBLock.lock();
+		final String query = "SELECT full_description, time, file_id, GROUP_CONCAT(events.event_id), GROUP_CONCAT(sub_type)"
+				+ " FROM events " + useHashHitTablesHelper(filter) + useTagTablesHelper(filter)
+				+ " WHERE time >= " + startTime + " AND time <" + endTime + " AND " + getSQLWhere(filter)
+				+ " GROUP BY time,full_description, file_id ORDER BY time ASC, full_description";
+		Statement stmt = null;
+		ResultSet rs = null;
+		try {
+
+			stmt = con.createStatement();
+			rs = stmt.executeQuery(query);
+
+			while (rs.next()) {
+
+				//make a map from event type to event ID
+				List<Long> eventIDs = unGroupConcat(rs.getString("GROUP_CONCAT(events.event_id)"), Long::valueOf);
+				List<EventType> eventTypes = unGroupConcat(rs.getString("GROUP_CONCAT(sub_type)"), (String s) -> RootEventType.allTypes.get(Integer.valueOf(s)));
+				Map<EventType, Long> eventMap = new HashMap<EventType, Long>();
+				for (int i = 0; i < eventIDs.size(); i++) {
+					eventMap.put(eventTypes.get(i), eventIDs.get(i));
+				}
+				results.add(new CombinedEvent(rs.getLong("time") * 1000, rs.getString("full_description"), rs.getLong("file_id"), eventMap));
+			}
+
+		} catch (SQLException sqlEx) {
+			LOGGER.log(Level.SEVERE, "failed to execute query for combined events", sqlEx); // NON-NLS
+		} finally {
+			closeResultSet(rs);
+			closeStatement(stmt);
+			DBLock.unlock();
+		}
+
+		return results;
+	}
+
+	/**
+	 * this relies on the fact that no tskObj has ID 0 but 0 is the default
+	 * value for the datasource_id column in the events table.
+	 */
+	public boolean hasNewColumns() {
+		return hasHashHitColumn() && hasDataSourceIDColumn() && hasTaggedColumn()
+				&& (getDataSourceIDs().isEmpty() == false);
+	}
+
+	public Set<Long> getDataSourceIDs() {
+		HashSet<Long> hashSet = new HashSet<Long>();
+		DBLock.lock();
+		ResultSet rs = null;
+		try {
+			rs = getDataSourceIDsStmt.executeQuery();
+			while (rs.next()) {
+				long datasourceID = rs.getLong("datasource_id"); //NON-NLS
+				hashSet.add(datasourceID);
+			}
+		} catch (SQLException ex) {
+			LOGGER.log(Level.SEVERE, "Failed to get MAX time.", ex); // NON-NLS
+		} finally {
+
+			closeResultSet(rs);
+			DBLock.unlock();
+		}
+		return hashSet;
+	}
+
+	public Map<Long, String> getHashSetNames() {
+		Map<Long, String> hashSets = new HashMap<Long, String>();
+		DBLock.lock();
+		ResultSet rs = null;
+		try {
+			rs = getHashSetNamesStmt.executeQuery();
+			while (rs.next()) {
+				long hashSetID = rs.getLong("hash_set_id"); //NON-NLS
+				String hashSetName = rs.getString("hash_set_name"); //NON-NLS
+				hashSets.put(hashSetID, hashSetName);
+			}
+		} catch (SQLException ex) {
+			LOGGER.log(Level.SEVERE, "Failed to get hash sets.", ex); // NON-NLS
+		} finally {
+			closeResultSet(rs);
+			DBLock.unlock();
+		}
+		return Collections.unmodifiableMap(hashSets);
+	}
+
+	public void analyze() {
+		DBLock.lock();
+		Statement createStatement = null;
+		try {
+			createStatement = con.createStatement();
+
+			boolean b = createStatement.execute("analyze; analyze sqlite_master;"); //NON-NLS
+		} catch (SQLException ex) {
+			LOGGER.log(Level.SEVERE, "Failed to analyze events db.", ex); // NON-NLS
+		} finally {
+			closeStatement(createStatement);
+			DBLock.unlock();
+		}
+	}
+
+	/**
+	 * @return maximum time in seconds from unix epoch
+	 */
+	public Long getMaxTime() {
+		DBLock.lock();
+		ResultSet rs = null;
+		try {
+			rs = getMaxTimeStmt.executeQuery();
+			while (rs.next()) {
+				return rs.getLong("max"); // NON-NLS
+			}
+		} catch (SQLException ex) {
+			LOGGER.log(Level.SEVERE, "Failed to get MAX time.", ex); // NON-NLS
+		} finally {
+			closeResultSet(rs);
+			DBLock.unlock();
+		}
+		return -1l;
+	}
+
+	/**
+	 * @return maximum time in seconds from unix epoch
+	 */
+	public Long getMinTime() {
+		DBLock.lock();
+		ResultSet rs = null;
+		try {
+			rs = getMinTimeStmt.executeQuery();
+			while (rs.next()) {
+				return rs.getLong("min"); // NON-NLS
+			}
+		} catch (SQLException ex) {
+			LOGGER.log(Level.SEVERE, "Failed to get MIN time.", ex); // NON-NLS
+		} finally {
+			closeResultSet(rs);
+			DBLock.unlock();
+		}
+		return -1l;
+	}
+
+	/**
+	 * 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
+	 */
+	final synchronized void initializeDB() throws TskCoreException {
+
+		if (con == null || con.isOpen() == false) {
+			con = sleuthkitCase.getConnection();
+		}
+		try {
+			configureDB();
+		} catch (SQLException ex) {
+			LOGGER.log(Level.SEVERE, "problem accessing  database", ex); // NON-NLS
+			return;
+		}
+
+		DBLock.lock();
+		try {
+
+			///TODO: Move to SleuthkitCase
+			Statement stmt = null;
+			stmt = con.createStatement();
+			try {
+				String sql = "CREATE TABLE if not exists db_info " // NON-NLS
+						+ " ( key TEXT, " // NON-NLS
+						+ " value INTEGER, " // NON-NLS
+						+ "PRIMARY KEY (key))"; // NON-NLS
+				stmt.execute(sql);
+			} catch (SQLException ex) {
+				LOGGER.log(Level.SEVERE, "problem creating db_info table", ex); // NON-NLS
+			}
+
+			try {
+				String sql = "CREATE TABLE if not exists events " // NON-NLS
+						+ " (event_id INTEGER PRIMARY KEY, " // NON-NLS
+						+ " datasource_id INTEGER, " // NON-NLS
+						+ " file_id INTEGER, " // NON-NLS
+						+ " artifact_id INTEGER, " // NON-NLS
+						+ " time INTEGER, " // NON-NLS
+						+ " sub_type INTEGER, " // NON-NLS
+						+ " base_type INTEGER, " // NON-NLS
+						+ " full_description TEXT, " // NON-NLS
+						+ " med_description TEXT, " // NON-NLS
+						+ " short_description TEXT, " // NON-NLS
+						+ " known_state INTEGER," //boolean // NON-NLS
+						+ " hash_hit INTEGER," //boolean // NON-NLS
+						+ " tagged INTEGER)"; //boolean // NON-NLS
+				stmt.execute(sql);
+			} catch (SQLException ex) {
+				LOGGER.log(Level.SEVERE, "problem creating  database table", ex); // NON-NLS
+			}
+
+			if (hasDataSourceIDColumn() == false) {
+				try {
+					String sql = "ALTER TABLE events ADD COLUMN datasource_id INTEGER"; // NON-NLS
+					stmt.execute(sql);
+				} catch (SQLException ex) {
+					LOGGER.log(Level.SEVERE, "problem upgrading events table", ex); // NON-NLS
+				}
+			}
+			if (hasTaggedColumn() == false) {
+				try {
+					String sql = "ALTER TABLE events ADD COLUMN tagged INTEGER"; // NON-NLS
+					stmt.execute(sql);
+				} catch (SQLException ex) {
+					LOGGER.log(Level.SEVERE, "problem upgrading events table", ex); // NON-NLS
+				}
+			}
+
+			if (hasHashHitColumn() == false) {
+				try {
+					String sql = "ALTER TABLE events ADD COLUMN hash_hit INTEGER"; // NON-NLS
+					stmt.execute(sql);
+				} catch (SQLException ex) {
+					LOGGER.log(Level.SEVERE, "problem upgrading events table", ex); // NON-NLS
+				}
+			}
+
+			try {
+				String sql = "CREATE TABLE  if not exists hash_sets " //NON-NLS
+						+ "( hash_set_id INTEGER primary key," //NON-NLS
+						+ " hash_set_name VARCHAR(255) UNIQUE NOT NULL)"; //NON-NLS
+				stmt.execute(sql);
+			} catch (SQLException ex) {
+				LOGGER.log(Level.SEVERE, "problem creating hash_sets table", ex); //NON-NLS
+			}
+
+			try {
+				String sql = "CREATE TABLE  if not exists hash_set_hits " //NON-NLS
+						+ "(hash_set_id INTEGER REFERENCES hash_sets(hash_set_id) not null, " //NON-NLS
+						+ " event_id INTEGER REFERENCES events(event_id) not null, " //NON-NLS
+						+ " PRIMARY KEY (hash_set_id, event_id))"; //NON-NLS
+				stmt.execute(sql);
+			} catch (SQLException ex) {
+				LOGGER.log(Level.SEVERE, "problem creating hash_set_hits table", ex); //NON-NLS
+			}
+
+			initializeTagsTable();
+
+			createIndex("events", Arrays.asList("datasource_id")); //NON-NLS
+			createIndex("events", Arrays.asList("event_id", "hash_hit")); //NON-NLS
+			createIndex("events", Arrays.asList("event_id", "tagged")); //NON-NLS
+			createIndex("events", Arrays.asList("file_id")); //NON-NLS
+			createIndex("events", Arrays.asList("artifact_id")); //NON-NLS
+			createIndex("events", Arrays.asList("sub_type", "short_description", "time")); //NON-NLS
+			createIndex("events", Arrays.asList("base_type", "short_description", "time")); //NON-NLS
+			createIndex("events", Arrays.asList("time")); //NON-NLS
+			createIndex("events", Arrays.asList("known_state")); //NON-NLS
+
+			try {
+				insertRowStmt = prepareStatement(
+						"INSERT INTO events (datasource_id,file_id ,artifact_id, time, sub_type, base_type, full_description, med_description, short_description, known_state, hash_hit, tagged) " // NON-NLS
+						+ "VALUES (?,?,?,?,?,?,?,?,?,?,?,?)"); // NON-NLS
+				getHashSetNamesStmt = prepareStatement("SELECT hash_set_id, hash_set_name FROM hash_sets"); // NON-NLS
+				getDataSourceIDsStmt = prepareStatement("SELECT DISTINCT datasource_id FROM events WHERE datasource_id != 0"); // NON-NLS
+				getMaxTimeStmt = prepareStatement("SELECT Max(time) AS max FROM events"); // NON-NLS
+				getMinTimeStmt = prepareStatement("SELECT Min(time) AS min FROM events"); // NON-NLS
+				getEventByIDStmt = prepareStatement("SELECT * FROM events WHERE event_id =  ?"); // NON-NLS
+				insertHashSetStmt = prepareStatement("INSERT OR IGNORE INTO hash_sets (hash_set_name)  values (?)"); //NON-NLS
+				selectHashSetStmt = prepareStatement("SELECT hash_set_id FROM hash_sets WHERE hash_set_name = ?"); //NON-NLS
+				insertHashHitStmt = prepareStatement("INSERT OR IGNORE INTO hash_set_hits (hash_set_id, event_id) values (?,?)"); //NON-NLS
+				insertTagStmt = prepareStatement("INSERT OR IGNORE INTO tags (tag_id, tag_name_id,tag_name_display_name, event_id) values (?,?,?,?)"); //NON-NLS
+				deleteTagStmt = prepareStatement("DELETE FROM tags WHERE tag_id = ?"); //NON-NLS
+
+				/*
+				 * This SQL query is really just a select count(*), but that has
+				 * performance problems on very large tables unless you include
+				 * a where clause see http://stackoverflow.com/a/9338276/4004683
+				 * for more.
+				 */
+				countAllEventsStmt = prepareStatement("SELECT count(event_id) AS count FROM events WHERE event_id IS NOT null"); //NON-NLS
+				dropEventsTableStmt = prepareStatement("DROP TABLE IF EXISTS events"); //NON-NLS
+				dropHashSetHitsTableStmt = prepareStatement("DROP TABLE IF EXISTS hash_set_hits"); //NON-NLS
+				dropHashSetsTableStmt = prepareStatement("DROP TABLE IF EXISTS hash_sets"); //NON-NLS
+				dropTagsTableStmt = prepareStatement("DROP TABLE IF EXISTS tags"); //NON-NLS
+				dropDBInfoTableStmt = prepareStatement("DROP TABLE IF EXISTS db_ino"); //NON-NLS
+				selectNonArtifactEventIDsByObjectIDStmt = prepareStatement("SELECT event_id FROM events WHERE file_id == ? AND artifact_id IS NULL"); //NON-NLS
+				selectEventIDsBYObjectAndArtifactIDStmt = prepareStatement("SELECT event_id FROM events WHERE file_id == ? AND artifact_id = ?"); //NON-NLS
+			} catch (SQLException sQLException) {
+				LOGGER.log(Level.SEVERE, "failed to prepareStatment", sQLException); // NON-NLS
+			}
+		} catch (SQLException ex) {
+			Logger.getLogger(EventDB.class.getName()).log(Level.SEVERE, null, ex);
+		} finally {
+			DBLock.unlock();
+		}
+	}
+
+	/**
+	 * Get a List of event IDs for the events that are derived from the given
+	 * artifact.
+	 *
+	 * @param artifact The BlackboardArtifact to get derived event IDs for.
+	 *
+	 * @return A List of event IDs for the events that are derived from the
+	 *         given artifact.
+	 */
+	public List<Long> getEventIDsForArtifact(BlackboardArtifact artifact) {
+		DBLock.lock();
+
+		String query = "SELECT event_id FROM events WHERE artifact_id == " + artifact.getArtifactID();
+
+		ArrayList<Long> results = new ArrayList<Long>();
+		Statement stmt = null;
+		ResultSet rs = null;
+		try {
+			stmt = con.createStatement();
+			rs = stmt.executeQuery(query);
+			while (rs.next()) {
+				results.add(rs.getLong("event_id"));
+			}
+		} catch (SQLException ex) {
+			LOGGER.log(Level.SEVERE, "Error executing getEventIDsForArtifact query.", ex); // NON-NLS
+		} finally {
+			closeResultSet(rs);
+			closeStatement(stmt);
+			DBLock.unlock();
+		}
+		return results;
+	}
+
+	/**
+	 * Get a List of event IDs for the events that are derived from the given
+	 * file.
+	 *
+	 * @param file                    The AbstractFile to get derived event IDs
+	 *                                for.
+	 * @param includeDerivedArtifacts If true, also get event IDs for events
+	 *                                derived from artifacts derived form this
+	 *                                file. If false, only gets events derived
+	 *                                directly from this file (file system
+	 *                                timestamps).
+	 *
+	 * @return A List of event IDs for the events that are derived from the
+	 *         given file.
+	 */
+	public List<Long> getEventIDsForFile(AbstractFile file, boolean includeDerivedArtifacts) {
+		DBLock.lock();
+
+		String query = "SELECT event_id FROM events WHERE file_id == " + file.getId()
+				+ (includeDerivedArtifacts ? "" : " AND artifact_id IS NULL");
+
+		ArrayList<Long> results = new ArrayList<Long>();
+
+		Statement stmt = null;
+		ResultSet rs = null;
+		try {
+
+			stmt = con.createStatement();
+			rs = stmt.executeQuery(query);
+			while (rs.next()) {
+				results.add(rs.getLong("event_id"));
+			}
+		} catch (SQLException ex) {
+			LOGGER.log(Level.SEVERE, "Error executing getEventIDsForFile query.", ex); // NON-NLS
+		} finally {
+			closeResultSet(rs);
+			closeStatement(stmt);
+			DBLock.unlock();
+		}
+		return results;
+	}
+
+	/**
+	 * create the tags table if it doesn't already exist. This is broken out as
+	 * a separate method so it can be used by reInitializeTags()
+	 */
+	private void initializeTagsTable() {
+		String sql = "CREATE TABLE IF NOT EXISTS tags " //NON-NLS
+				+ "(tag_id INTEGER NOT NULL," //NON-NLS
+				+ " tag_name_id INTEGER NOT NULL, " //NON-NLS
+				+ " tag_name_display_name TEXT NOT NULL, " //NON-NLS
+				+ " event_id INTEGER REFERENCES events(event_id) NOT NULL, " //NON-NLS
+				+ " PRIMARY KEY (event_id, tag_name_id))"; //NON-NLS
+		Statement stmt = null;
+		try {
+			stmt = con.createStatement();
+			stmt.execute(sql);
+		} catch (SQLException ex) {
+			LOGGER.log(Level.SEVERE, "problem creating tags table", ex); //NON-NLS
+		} finally {
+			closeStatement(stmt);
+		}
+	}
+
+	/**
+	 *
+	 * @param tableName  the value of tableName
+	 * @param columnList the value of columnList
+	 */
+	private void createIndex(final String tableName, final List<String> columnList) {
+		String indexColumns = columnList.stream().collect(Collectors.joining(",", "(", ")"));
+		String indexName = tableName + "_" + StringUtils.joinAsStrings(columnList, "_") + "_idx"; //NON-NLS
+		Statement stmt = null;
+		try {
+
+			stmt = con.createStatement();
+			String sql = "CREATE INDEX IF NOT EXISTS " + indexName + " ON " + tableName + indexColumns; // NON-NLS
+			stmt.execute(sql);
+		} catch (SQLException ex) {
+			LOGGER.log(Level.SEVERE, "problem creating index " + indexName, ex); // NON-NLS
+		} finally {
+			closeStatement(stmt);
+		}
+	}
+
+	/**
+	 * @param dbColumn the value of dbColumn
+	 *
+	 * @return the boolean
+	 */
+	private boolean hasDBColumn(final String dbColumn) {
+		Statement stmt = null;
+		try {
+			stmt = con.createStatement();
+
+			ResultSet executeQuery = stmt.executeQuery("PRAGMA table_info(events)"); //NON-NLS
+			while (executeQuery.next()) {
+				if (dbColumn.equals(executeQuery.getString("name"))) {
+					return true;
+				}
+			}
+		} catch (SQLException ex) {
+			LOGGER.log(Level.SEVERE, "problem executing pragma", ex); // NON-NLS
+		}
+		return false;
+	}
+
+	private boolean hasDataSourceIDColumn() {
+		return hasDBColumn("datasource_id"); //NON-NLS
+	}
+
+	private boolean hasTaggedColumn() {
+		return hasDBColumn("tagged"); //NON-NLS
+	}
+
+	private boolean hasHashHitColumn() {
+		return hasDBColumn("hash_hit"); //NON-NLS
+	}
+
+	void insertEvent(long time, EventType type, long datasourceID, long objID,
+			Long artifactID, String fullDescription, String medDescription,
+			String shortDescription, TskData.FileKnown known, Set<String> hashSets, List<? extends Tag> tags) {
+
+		EventTransaction transaction = beginTransaction();
+		insertEvent(time, type, datasourceID, objID, artifactID, fullDescription, medDescription, shortDescription, known, hashSets, tags, transaction);
+		commitTransaction(transaction);
+	}
+
+	/**
+	 * use transactions to update files
+	 *
+	 * @param f
+	 * @param transaction
+	 */
+	public void insertEvent(long time, EventType type, long datasourceID, long objID,
+			Long artifactID, String fullDescription, String medDescription,
+			String shortDescription, TskData.FileKnown known, Set<String> hashSetNames,
+			List<? extends Tag> tags, EventTransaction transaction) {
+
+		if (transaction.isClosed()) {
+			throw new IllegalArgumentException("can't update database with closed transaction"); // NON-NLS
+		}
+		int typeNum = RootEventType.allTypes.indexOf(type);
+		int superTypeNum = type.getSuperType().ordinal();
+
+		DBLock.lock();
+		try {
+
+			//"INSERT INTO events (datasource_id,file_id ,artifact_id, time, sub_type, base_type, full_description, med_description, short_description, known_state, hashHit, tagged) " 
+			insertRowStmt.clearParameters();
+			insertRowStmt.setLong(1, datasourceID);
+			insertRowStmt.setLong(2, objID);
+			if (artifactID != null) {
+				insertRowStmt.setLong(3, artifactID);
+			} else {
+				insertRowStmt.setNull(3, Types.NULL);
+			}
+			insertRowStmt.setLong(4, time);
+
+			if (typeNum != -1) {
+				insertRowStmt.setInt(5, typeNum);
+			} else {
+				insertRowStmt.setNull(5, Types.INTEGER);
+			}
+
+			insertRowStmt.setInt(6, superTypeNum);
+			insertRowStmt.setString(7, fullDescription);
+			insertRowStmt.setString(8, medDescription);
+			insertRowStmt.setString(9, shortDescription);
+
+			insertRowStmt.setByte(10, known == null ? TskData.FileKnown.UNKNOWN.getFileKnownValue() : known.getFileKnownValue());
+
+			insertRowStmt.setInt(11, hashSetNames.isEmpty() ? 0 : 1);
+			insertRowStmt.setInt(12, tags.isEmpty() ? 0 : 1);
+
+			insertRowStmt.executeUpdate();
+
+			ResultSet generatedKeys = null;
+			try {
+				generatedKeys = insertRowStmt.getGeneratedKeys();
+
+				while (generatedKeys.next()) {
+					long eventID = generatedKeys.getLong("last_insert_rowid()"); //NON-NLS
+					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);
+						ResultSet rs = null;
+						try {
+							rs = selectHashSetStmt.executeQuery();
+							while (rs.next()) {
+								int hashsetID = rs.getInt("hash_set_id"); //NON-NLS
+								//"insert or ignore into hash_set_hits (hash_set_id, obj_id) values (?,?)";
+								insertHashHitStmt.setInt(1, hashsetID);
+								insertHashHitStmt.setLong(2, eventID);
+								insertHashHitStmt.executeUpdate();
+								break;
+							}
+						} finally {
+							closeResultSet(rs);
+						}
+					}
+					for (Tag tag : tags) {
+						//could this be one insert?  is there a performance win?
+						insertTag(tag, eventID);
+					}
+					break;
+				}
+			} finally {
+				closeResultSet(generatedKeys);
+			}
+
+		} catch (SQLException ex) {
+			LOGGER.log(Level.SEVERE, "failed to insert event", ex); // NON-NLS
+		} finally {
+			DBLock.unlock();
+		}
+	}
+
+	/**
+	 * mark any events with the given object and artifact ids as tagged, and
+	 * record the tag it self.
+	 *
+	 * @param objectID   the obj_id that this tag applies to, the id of the
+	 *                   content that the artifact is derived from for artifact
+	 *                   tags
+	 * @param artifactID the artifact_id that this tag applies to, or null if
+	 *                   this is a content tag
+	 * @param tag        the tag that should be inserted
+	 *
+	 * @return the event ids that match the object/artifact pair
+	 */
+	public Set<Long> addTag(long objectID, Long artifactID, Tag tag, EventTransaction transaction) {
+		if (transaction != null && transaction.isClosed()) {
+			throw new IllegalArgumentException("can't update database with closed transaction"); // NON-NLS
+		}
+		DBLock.lock();
+		try {
+			Set<Long> eventIDs = markEventsTagged(objectID, artifactID, true);
+			for (Long eventID : eventIDs) {
+				insertTag(tag, eventID);
+			}
+			return eventIDs;
+		} catch (SQLException ex) {
+			LOGGER.log(Level.SEVERE, "failed to add tag to event", ex); // NON-NLS
+		} finally {
+			DBLock.unlock();
+		}
+		return Collections.emptySet();
+	}
+
+	/**
+	 * insert this tag into the db
+	 * <p>
+	 * NOTE: does not lock the db, must be called form inside a
+	 * DBLock.lock/unlock pair
+	 *
+	 * @param tag     the tag to insert
+	 * @param eventID the event id that this tag is applied to.
+	 *
+	 * @throws SQLException if there was a problem executing insert
+	 */
+	private void insertTag(Tag tag, long eventID) throws SQLException {
+
+		//"INSERT OR IGNORE INTO tags (tag_id, tag_name_id,tag_name_display_name, event_id) values (?,?,?,?)"
+		insertTagStmt.clearParameters();
+		insertTagStmt.setLong(1, tag.getId());
+		insertTagStmt.setLong(2, tag.getName().getId());
+		insertTagStmt.setString(3, tag.getName().getDisplayName());
+		insertTagStmt.setLong(4, eventID);
+		insertTagStmt.executeUpdate();
+	}
+
+	/**
+	 * mark any events with the given object and artifact ids as tagged, and
+	 * record the tag it self.
+	 *
+	 * @param objectID    the obj_id that this tag applies to, the id of the
+	 *                    content that the artifact is derived from for artifact
+	 *                    tags
+	 * @param artifactID  the artifact_id that this tag applies to, or null if
+	 *                    this is a content tag
+	 * @param tag         the tag that should be deleted
+	 * @param stillTagged true if there are other tags still applied to this
+	 *                    event in autopsy
+	 *
+	 * @return the event ids that match the object/artifact pair
+	 */
+	public Set<Long> deleteTag(long objectID, Long artifactID, long tagID, boolean stillTagged) {
+		DBLock.lock();
+		try {
+			//"DELETE FROM tags WHERE tag_id = ?
+			deleteTagStmt.clearParameters();
+			deleteTagStmt.setLong(1, tagID);
+			deleteTagStmt.executeUpdate();
+
+			return markEventsTagged(objectID, artifactID, stillTagged);
+		} catch (SQLException ex) {
+			LOGGER.log(Level.SEVERE, "failed to add tag to event", ex); // NON-NLS
+		} finally {
+			DBLock.unlock();
+		}
+		return Collections.emptySet();
+	}
+
+	/**
+	 * mark any events with the given object and artifact ids as tagged, and
+	 * record the tag it self.
+	 * <p>
+	 * NOTE: does not lock the db, must be called form inside a
+	 * DBLock.lock/unlock pair
+	 *
+	 * @param objectID   the obj_id that this tag applies to, the id of the
+	 *                   content that the artifact is derived from for artifact
+	 *                   tags
+	 * @param artifactID the artifact_id that this tag applies to, or null if
+	 *                   this is a content tag
+	 * @param tagged     true to mark the matching events tagged, false to mark
+	 *                   them as untagged
+	 *
+	 * @return the event ids that match the object/artifact pair
+	 *
+	 * @throws SQLException if there is an error marking the events as
+	 *                      (un)taggedS
+	 */
+	private Set<Long> markEventsTagged(long objectID, Long artifactID, boolean tagged) throws SQLException {
+
+		PreparedStatement selectStmt;
+		if (Objects.isNull(artifactID)) {
+			//"SELECT event_id FROM events WHERE file_id == ? AND artifact_id IS NULL"
+			selectNonArtifactEventIDsByObjectIDStmt.clearParameters();
+			selectNonArtifactEventIDsByObjectIDStmt.setLong(1, objectID);
+			selectStmt = selectNonArtifactEventIDsByObjectIDStmt;
+		} else {
+			//"SELECT event_id FROM events WHERE file_id == ? AND artifact_id = ?"
+			selectEventIDsBYObjectAndArtifactIDStmt.clearParameters();
+			selectEventIDsBYObjectAndArtifactIDStmt.setLong(1, objectID);
+			selectEventIDsBYObjectAndArtifactIDStmt.setLong(2, artifactID);
+			selectStmt = selectEventIDsBYObjectAndArtifactIDStmt;
+		}
+
+		HashSet<Long> eventIDs = new HashSet<>();
+		try (ResultSet executeQuery = selectStmt.executeQuery();) {
+			while (executeQuery.next()) {
+				eventIDs.add(executeQuery.getLong("event_id")); //NON-NLS
+			}
+		}
+
+		//update tagged state for all event with selected ids
+		try (Statement updateStatement = con.createStatement();) {
+			updateStatement.executeUpdate("UPDATE events SET tagged = " + (tagged ? 1 : 0) //NON-NLS
+					+ " WHERE event_id IN (" + StringUtils.joinAsStrings(eventIDs, ",") + ")"); //NON-NLS
+		}
+
+		return eventIDs;
+	}
+
+	void rollBackTransaction(EventTransaction trans) {
+		trans.rollback();
+	}
+
+	private void closeStatements() throws SQLException {
+		for (PreparedStatement pStmt : preparedStatements) {
+			pStmt.close();
+		}
+	}
+
+	private void configureDB() throws SQLException {
+		DBLock.lock();
+		//this should match Sleuthkit db setup
+		try (Statement statement = con.createStatement()) {
+			//reduce i/o operations, we have no OS crash recovery anyway
+			statement.execute("PRAGMA synchronous = OFF;"); // NON-NLS
+			//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;"); // NON-NLS
+			//this made a big difference to query speed
+			statement.execute("PRAGMA temp_store = MEMORY"); // NON-NLS
+			//this made a modest improvement in query speeds
+			statement.execute("PRAGMA cache_size = 50000"); // NON-NLS
+			//we never delete anything so...
+			statement.execute("PRAGMA auto_vacuum = 0"); // NON-NLS
+			//allow to query while in transaction - no need read locks
+			statement.execute("PRAGMA read_uncommitted = True;"); // NON-NLS
+		} finally {
+			DBLock.unlock();
+		}
+
+		try {
+			LOGGER.log(Level.INFO, String.format("sqlite-jdbc version %s loaded in %s mode", // NON-NLS
+					SQLiteJDBCLoader.getVersion(), SQLiteJDBCLoader.isNativeMode() ? "native" : "pure-java")); // NON-NLS
+		} catch (Exception exception) {
+			LOGGER.log(Level.SEVERE, "Failed to determine if sqlite-jdbc is loaded in native or pure-java mode.", exception); //NON-NLS
+		}
+	}
+
+	private SingleEvent constructTimeLineEvent(ResultSet rs) throws SQLException {
+		return new SingleEvent(rs.getLong("event_id"), //NON-NLS
+				rs.getLong("datasource_id"), //NON-NLS
+				rs.getLong("file_id"), //NON-NLS
+				rs.getLong("artifact_id"), //NON-NLS
+				rs.getLong("time"), RootEventType.allTypes.get(rs.getInt("sub_type")), //NON-NLS
+				rs.getString("full_description"), //NON-NLS
+				rs.getString("med_description"), //NON-NLS
+				rs.getString("short_description"), //NON-NLS
+				TskData.FileKnown.valueOf(rs.getByte("known_state")), //NON-NLS
+				rs.getInt("hash_hit") != 0, //NON-NLS
+				rs.getInt("tagged") != 0); //NON-NLS
+	}
+
+	/**
+	 * count all the events with the given options and return a map organizing
+	 * the counts in a hierarchy from date > eventtype> count
+	 *
+	 * @param startTime events before this time will be excluded (seconds from
+	 *                  unix epoch)
+	 * @param endTime   events at or after this time will be excluded (seconds
+	 *                  from unix epoch)
+	 * @param filter    only events that pass this filter will be counted
+	 * @param zoomLevel only events of this type or a subtype will be counted
+	 *                  and the counts will be organized into bins for each of
+	 *                  the subtypes of the given event type
+	 *
+	 * @return a map organizing the counts in a hierarchy from date > eventtype>
+	 *         count
+	 */
+	private Map<EventType, Long> countEventsByType(Long startTime, Long endTime, RootFilter filter, EventTypeZoomLevel zoomLevel) {
+		if (Objects.equals(startTime, endTime)) {
+			endTime++;
+		}
+
+		Map<EventType, Long> typeMap = new HashMap<>();
+
+		//do we want the root or subtype column of the databse
+		final boolean useSubTypes = (zoomLevel == EventTypeZoomLevel.SUB_TYPE);
+
+		//get some info about the range of dates requested
+		final String queryString = "SELECT count(DISTINCT events.event_id) AS count, " + typeColumnHelper(useSubTypes) //NON-NLS
+				+ " FROM events" + useHashHitTablesHelper(filter) + useTagTablesHelper(filter) + " WHERE time >= " + startTime + " AND time < " + endTime + " AND " + getSQLWhere(filter) // NON-NLS
+				+ " GROUP BY " + typeColumnHelper(useSubTypes); // NON-NLS
+
+		DBLock.lock();
+		try (Statement stmt = con.createStatement();
+				ResultSet rs = stmt.executeQuery(queryString);) {
+			while (rs.next()) {
+				EventType type = useSubTypes
+						? RootEventType.allTypes.get(rs.getInt("sub_type")) //NON-NLS
+						: BaseTypes.values()[rs.getInt("base_type")]; //NON-NLS
+
+				typeMap.put(type, rs.getLong("count")); // NON-NLS
+			}
+
+		} catch (Exception ex) {
+			LOGGER.log(Level.SEVERE, "Error getting count of events from db.", ex); // NON-NLS
+		} finally {
+			DBLock.unlock();
+		}
+		return typeMap;
+	}
+
+	/**
+	 * get a list of {@link EventStripe}s, clustered according to the given zoom
+	 * paramaters.
+	 *
+	 * @param params the {@link ZoomParams} that determine the zooming,
+	 *               filtering and clustering.
+	 *
+	 * @return a list of aggregate events within the given timerange, that pass
+	 *         the supplied filter, aggregated according to the given event type
+	 *         and description zoom levels
+	 */
+	public List<EventStripe> getEventStripes(ZoomParams params, DateTimeZone tz) {
+		//unpack params
+		Interval timeRange = params.getTimeRange();
+		RootFilter filter = params.getFilter();
+		DescriptionLoD descriptionLOD = params.getDescriptionLOD();
+		EventTypeZoomLevel typeZoomLevel = params.getTypeZoomLevel();
+
+		long start = timeRange.getStartMillis() / 1000;
+		long end = timeRange.getEndMillis() / 1000;
+
+		//ensure length of querried interval is not 0
+		end = Math.max(end, start + 1);
+
+		//get some info about the time range requested
+		RangeDivisionInfo rangeInfo = RangeDivisionInfo.getRangeDivisionInfo(timeRange, tz);
+
+		//build dynamic parts of query
+		String strfTimeFormat = getStrfTimeFormat(rangeInfo.getPeriodSize());
+		String descriptionColumn = getDescriptionColumn(descriptionLOD);
+		final boolean useSubTypes = typeZoomLevel.equals(EventTypeZoomLevel.SUB_TYPE);
+		String timeZone = tz.equals(DateTimeZone.getDefault()) ? ", 'localtime'" : "";  // NON-NLS
+		String typeColumn = typeColumnHelper(useSubTypes);
+
+		//compose query string, the new-lines are only for nicer formatting if printing the entire query
+		String query = "SELECT strftime('" + strfTimeFormat + "',time , 'unixepoch'" + timeZone + ") AS interval," // NON-NLS
+				+ "\n group_concat(events.event_id) as event_ids," //NON-NLS
+				+ "\n group_concat(CASE WHEN hash_hit = 1 THEN events.event_id ELSE NULL END) as hash_hits," //NON-NLS
+				+ "\n group_concat(CASE WHEN tagged = 1 THEN events.event_id ELSE NULL END) as taggeds," //NON-NLS
+				+ "\n min(time), max(time),  " + typeColumn + ", " + descriptionColumn // NON-NLS
+				+ "\n FROM events" + useHashHitTablesHelper(filter) + useTagTablesHelper(filter) // NON-NLS
+				+ "\n WHERE time >= " + start + " AND time < " + end + " AND " + getSQLWhere(filter) // NON-NLS
+				+ "\n GROUP BY interval, " + typeColumn + " , " + descriptionColumn // NON-NLS
+				+ "\n ORDER BY min(time)"; // NON-NLS
+
+		// perform query and map results to AggregateEvent objects
+		List<EventCluster> events = new ArrayList<>();
+
+		DBLock.lock();
+		try (Statement createStatement = con.createStatement();
+				ResultSet rs = createStatement.executeQuery(query)) {
+			while (rs.next()) {
+				events.add(eventClusterHelper(rs, useSubTypes, descriptionLOD, filter.getTagsFilter(), tz));
+			}
+		} catch (SQLException ex) {
+			LOGGER.log(Level.SEVERE, "Failed to get events with query: " + query, ex); // NON-NLS
+		} finally {
+			DBLock.unlock();
+		}
+
+		return mergeClustersToStripes(rangeInfo.getPeriodSize().getPeriod(), events);
+	}
+
+	/**
+	 * map a single row in a ResultSet to an EventCluster
+	 *
+	 * @param rs             the result set whose current row should be mapped
+	 * @param useSubTypes    use the sub_type column if true, else use the
+	 *                       base_type column
+	 * @param descriptionLOD the description level of detail for this event
+	 * @param filter
+	 *
+	 * @return an AggregateEvent corresponding to the current row in the given
+	 *         result set
+	 *
+	 * @throws SQLException
+	 */
+	private EventCluster eventClusterHelper(ResultSet rs, boolean useSubTypes, DescriptionLoD descriptionLOD, TagsFilter filter, DateTimeZone tz) throws SQLException {
+		Interval interval = new Interval(rs.getLong("min(time)") * 1000, rs.getLong("max(time)") * 1000, tz);// NON-NLS
+		String eventIDsString = rs.getString("event_ids");// NON-NLS
+		List<Long> eventIDs = unGroupConcat(eventIDsString, Long::valueOf);
+		String description = rs.getString(getDescriptionColumn(descriptionLOD));
+		EventType type = useSubTypes ? RootEventType.allTypes.get(rs.getInt("sub_type")) : BaseTypes.values()[rs.getInt("base_type")];// NON-NLS
+
+		List<Long> hashHits = unGroupConcat(rs.getString("hash_hits"), Long::valueOf); //NON-NLS
+		List<Long> tagged = unGroupConcat(rs.getString("taggeds"), Long::valueOf); //NON-NLS
+
+		return new EventCluster(interval, type, eventIDs, hashHits, tagged, description, descriptionLOD);
+	}
+
+	/**
+	 * merge the events in the given list if they are within the same period
+	 * General algorithm is as follows:
+	 *
+	 * 1) sort them into a map from (type, description)-> List<aggevent>
+	 * 2) for each key in map, merge the events and accumulate them in a list to
+	 * return
+	 *
+	 * @param timeUnitLength
+	 * @param preMergedEvents
+	 *
+	 * @return
+	 */
+	static private List<EventStripe> mergeClustersToStripes(Period timeUnitLength, List<EventCluster> preMergedEvents) {
+
+		//effectively map from type to (map from description to events)
+		Map<EventType, SetMultimap< String, EventCluster>> typeMap = new HashMap<>();
+
+		for (EventCluster aggregateEvent : preMergedEvents) {
+			typeMap.computeIfAbsent(aggregateEvent.getEventType(), eventType -> HashMultimap.create())
+					.put(aggregateEvent.getDescription(), aggregateEvent);
+		}
+		//result list to return
+		ArrayList<EventCluster> aggEvents = new ArrayList<>();
+
+		//For each (type, description) key, merge agg events
+		for (SetMultimap<String, EventCluster> descrMap : typeMap.values()) {
+			//for each description ...
+			for (String descr : descrMap.keySet()) {
+				//run through the sorted events, merging together adjacent events
+				Iterator<EventCluster> iterator = descrMap.get(descr).stream()
+						.sorted(Comparator.comparing(event -> event.getSpan().getStartMillis()))
+						.iterator();
+				EventCluster current = iterator.next();
+				while (iterator.hasNext()) {
+					EventCluster next = iterator.next();
+					Interval gap = current.getSpan().gap(next.getSpan());
+
+					//if they overlap or gap is less one quarter timeUnitLength
+					//TODO: 1/4 factor is arbitrary. review! -jm
+					if (gap == null || gap.toDuration().getMillis() <= timeUnitLength.toDurationFrom(gap.getStart()).getMillis() / 4) {
+						//merge them
+						current = EventCluster.merge(current, next);
+					} else {
+						//done merging into current, set next as new current
+						aggEvents.add(current);
+						current = next;
+					}
+				}
+				aggEvents.add(current);
+			}
+		}
+
+		//merge clusters to stripes
+		Map<ImmutablePair<EventType, String>, EventStripe> stripeDescMap = new HashMap<>();
+
+		for (EventCluster eventCluster : aggEvents) {
+			stripeDescMap.merge(ImmutablePair.of(eventCluster.getEventType(), eventCluster.getDescription()),
+					new EventStripe(eventCluster), EventStripe::merge);
+		}
+
+		return stripeDescMap.values().stream().sorted(Comparator.comparing(EventStripe::getStartMillis)).collect(Collectors.toList());
+	}
+
+	private static String typeColumnHelper(final boolean useSubTypes) {
+		return useSubTypes ? "sub_type" : "base_type"; //NON-NLS
+	}
+
+	private PreparedStatement prepareStatement(String queryString) throws SQLException {
+		PreparedStatement prepareStatement = con.prepareStatement(queryString, 0);
+		preparedStatements.add(prepareStatement);
+		return prepareStatement;
+	}
+
+	/**
+	 * inner class that can reference access database connection
+	 */
+	public class EventTransaction {
+
+		private boolean closed = false;
+
+		/**
+		 * factory creation method
+		 *
+		 * @return a LogicalFileTransaction for the given connection
+		 *
+		 * @throws SQLException
+		 */
+		private EventTransaction() {
+
+			//get the write lock, released in close()
+			DBLock.lock();
+			try {
+				con.beginTransaction();
+			} catch (SQLException ex) {
+				LOGGER.log(Level.SEVERE, "failed to set auto-commit to to false", ex); // NON-NLS
+			}
+
+		}
+
+		private void rollback() {
+			if (!closed) {
+				try {
+					con.rollbackTransactionWithThrow();
+
+				} catch (SQLException ex1) {
+					LOGGER.log(Level.SEVERE, "Exception while attempting to rollback!!", ex1); // NON-NLS
+				} finally {
+					close();
+				}
+			}
+		}
+
+		private void commit() {
+			if (!closed) {
+				try {
+					con.commitTransaction();
+					// make sure we close before we update, bc they'll need locks
+					close();
+
+				} catch (SQLException ex) {
+
+					LOGGER.log(Level.SEVERE, "Error commiting events.db.", ex); // NON-NLS
+					rollback();
+				}
+			}
+		}
+
+		private void close() {
+			if (!closed) {
+				closed = true;//					con.setAutoCommit(true);
+				// NON-NLS
+				DBLock.unlock();
+			}
+		}
+
+		public Boolean isClosed() {
+			return closed;
+		}
+	}
+
+	/**
+	 * Static helper methods for converting between java "data model" objects
+	 * and sqlite queries.
+	 */
+	static String useHashHitTablesHelper(RootFilter filter) {
+		HashHitsFilter hashHitFilter = filter.getHashHitsFilter();
+		return hashHitFilter.isActive() ? " LEFT JOIN hash_set_hits " : " "; //NON-NLS
+	}
+
+	static String useTagTablesHelper(RootFilter filter) {
+		TagsFilter tagsFilter = filter.getTagsFilter();
+		return tagsFilter.isActive() ? " LEFT JOIN tags " : " "; //NON-NLS
+	}
+
+	/**
+	 * take the result of a group_concat SQLite operation and split it into a
+	 * set of X using the mapper to to convert from string to X
+	 *
+	 * @param <X>         the type of elements to return
+	 * @param groupConcat a string containing the group_concat result ( a comma
+	 *                    separated list)
+	 * @param mapper      a function from String to X
+	 *
+	 * @return a Set of X, each element mapped from one element of the original
+	 *         comma delimited string
+	 */
+	static <X> List<X> unGroupConcat(String groupConcat, Function<String, X> mapper) {
+		List<X> result = new ArrayList<X>();
+		String[] split = groupConcat.split(",");
+		for (String s : split) {
+			result.add(mapper.apply(s));
+		}
+		return result;
+	}
+
+	/**
+	 * get the SQL where clause corresponding to an intersection filter ie
+	 * (sub-clause1 and sub-clause2 and ... and sub-clauseN)
+	 *
+	 * @param filter the filter get the where clause for
+	 *
+	 * @return an SQL where clause (without the "where") corresponding to the
+	 *         filter
+	 */
+	private static String getSQLWhere(IntersectionFilter<?> filter) {
+		String join = String.join(" and ", filter.getSubFilters().stream()
+				.filter(Filter::isActive)
+				.map(EventDB::getSQLWhere)
+				.collect(Collectors.toList()));
+		return "(" + org.apache.commons.lang3.StringUtils.defaultIfBlank(join, "1") + ")";
+	}
+
+	/**
+	 * get the SQL where clause corresponding to a union filter ie (sub-clause1
+	 * or sub-clause2 or ... or sub-clauseN)
+	 *
+	 * @param filter the filter get the where clause for
+	 *
+	 * @return an SQL where clause (without the "where") corresponding to the
+	 *         filter
+	 */
+	private static String getSQLWhere(UnionFilter<?> filter) {
+		String join = String.join(" or ", filter.getSubFilters().stream()
+				.filter(Filter::isActive)
+				.map(EventDB::getSQLWhere)
+				.collect(Collectors.toList()));
+		return "(" + org.apache.commons.lang3.StringUtils.defaultIfBlank(join, "1") + ")";
+	}
+
+	public static String getSQLWhere(RootFilter filter) {
+		return getSQLWhere((Filter) filter);
+	}
+
+	/**
+	 * get the SQL where clause corresponding to the given filter
+	 *
+	 * uses instance of to dispatch to the correct method for each filter type.
+	 * NOTE: I don't like this if-else instance of chain, but I can't decide
+	 * what to do instead -jm
+	 *
+	 * @param filter a filter to generate the SQL where clause for
+	 *
+	 * @return an SQL where clause (without the "where") corresponding to the
+	 *         filter
+	 */
+	private static String getSQLWhere(Filter filter) {
+		String result = "";
+		if (filter == null) {
+			return "1";
+		} else if (filter instanceof DescriptionFilter) {
+			result = getSQLWhere((DescriptionFilter) filter);
+		} else if (filter instanceof TagsFilter) {
+			result = getSQLWhere((TagsFilter) filter);
+		} else if (filter instanceof HashHitsFilter) {
+			result = getSQLWhere((HashHitsFilter) filter);
+		} else if (filter instanceof DataSourceFilter) {
+			result = getSQLWhere((DataSourceFilter) filter);
+		} else if (filter instanceof DataSourcesFilter) {
+			result = getSQLWhere((DataSourcesFilter) filter);
+		} else if (filter instanceof HideKnownFilter) {
+			result = getSQLWhere((HideKnownFilter) filter);
+		} else if (filter instanceof HashHitsFilter) {
+			result = getSQLWhere((HashHitsFilter) filter);
+		} else if (filter instanceof TextFilter) {
+			result = getSQLWhere((TextFilter) filter);
+		} else if (filter instanceof TypeFilter) {
+			result = getSQLWhere((TypeFilter) filter);
+		} else if (filter instanceof IntersectionFilter) {
+			result = getSQLWhere((IntersectionFilter) filter);
+		} else if (filter instanceof UnionFilter) {
+			result = getSQLWhere((UnionFilter) filter);
+		} else {
+			throw new IllegalArgumentException("getSQLWhere not defined for " + filter.getClass().getCanonicalName());
+		}
+		result = org.apache.commons.lang3.StringUtils.deleteWhitespace(result).equals("(1and1and1)") ? "1" : result; //NON-NLS
+		result = org.apache.commons.lang3.StringUtils.deleteWhitespace(result).equals("()") ? "1" : result;
+		return result;
+	}
+
+	private static String getSQLWhere(HideKnownFilter filter) {
+		if (filter.isActive()) {
+			return "(known_state IS NOT '" + TskData.FileKnown.KNOWN.getFileKnownValue() + "')"; // NON-NLS
+		} else {
+			return "1";
+		}
+	}
+
+	private static String getSQLWhere(DescriptionFilter filter) {
+		if (filter.isActive()) {
+			String likeOrNotLike = (filter.getFilterMode() == DescriptionFilter.FilterMode.INCLUDE ? "" : " NOT") + " LIKE '"; //NON-NLS
+			return "(" + getDescriptionColumn(filter.getDescriptionLoD()) + likeOrNotLike + filter.getDescription() + "'  )"; // NON-NLS
+		} else {
+			return "1";
+		}
+	}
+
+	private static String getSQLWhere(TagsFilter filter) {
+		if (filter.isActive()
+				&& (filter.getSubFilters().isEmpty() == false)) {
+			String tagNameIDs = filter.getSubFilters().stream()
+					.filter((TagNameFilter t) -> t.isSelected() && !t.isDisabled())
+					.map((TagNameFilter t) -> String.valueOf(t.getTagName().getId()))
+					.collect(Collectors.joining(", ", "(", ")"));
+			return "(events.event_id == tags.event_id AND " //NON-NLS
+					+ "tags.tag_name_id IN " + tagNameIDs + ") "; //NON-NLS
+		} else {
+			return "1";
+		}
+
+	}
+
+	private static String getSQLWhere(HashHitsFilter filter) {
+		if (filter.isActive()
+				&& (filter.getSubFilters().isEmpty() == false)) {
+			String hashSetIDs = filter.getSubFilters().stream()
+					.filter((HashSetFilter t) -> t.isSelected() && !t.isDisabled())
+					.map((HashSetFilter t) -> String.valueOf(t.getHashSetID()))
+					.collect(Collectors.joining(", ", "(", ")"));
+			return "(hash_set_hits.hash_set_id IN " + hashSetIDs + " AND hash_set_hits.event_id == events.event_id)"; //NON-NLS
+		} else {
+			return "1";
+		}
+	}
+
+	private static String getSQLWhere(DataSourceFilter filter) {
+		if (filter.isActive()) {
+			return "(datasource_id = '" + filter.getDataSourceID() + "')"; //NON-NLS
+		} else {
+			return "1";
+		}
+	}
+
+	private static String getSQLWhere(DataSourcesFilter filter) {
+		return (filter.isActive()) ? "(datasource_id in (" //NON-NLS
+				+ filter.getSubFilters().stream()
+						.filter(AbstractFilter::isActive)
+						.map((dataSourceFilter) -> String.valueOf(dataSourceFilter.getDataSourceID()))
+						.collect(Collectors.joining(", ")) + "))" : "1";
+	}
+
+	private static String getSQLWhere(TextFilter filter) {
+		if (filter.isActive()) {
+			if (org.apache.commons.lang3.StringUtils.isBlank(filter.getText())) {
+				return "1";
+			}
+			String strippedFilterText = org.apache.commons.lang3.StringUtils.strip(filter.getText());
+			return "((med_description like '%" + strippedFilterText + "%')" //NON-NLS
+					+ " or (full_description like '%" + strippedFilterText + "%')" //NON-NLS
+					+ " or (short_description like '%" + strippedFilterText + "%'))"; //NON-NLS
+		} else {
+			return "1";
+		}
+	}
+
+	/**
+	 * generate a sql where clause for the given type filter, while trying to be
+	 * as simple as possible to improve performance.
+	 *
+	 * @param typeFilter
+	 *
+	 * @return
+	 */
+	private static String getSQLWhere(TypeFilter typeFilter) {
+		if (typeFilter.isSelected() == false) {
+			return "0";
+		} else if (typeFilter.getEventType() instanceof RootEventType) {
+			if (typeFilter.getSubFilters().stream()
+					.allMatch(subFilter -> subFilter.isActive() && subFilter.getSubFilters().stream().allMatch(Filter::isActive))) {
+				return "1"; //then collapse clause to true
+			}
+		}
+		return "(sub_type IN (" + org.apache.commons.lang3.StringUtils.join(getActiveSubTypes(typeFilter), ",") + "))"; //NON-NLS
+	}
+
+	private static List<Integer> getActiveSubTypes(TypeFilter filter) {
+		if (filter.isActive()) {
+			if (filter.getSubFilters().isEmpty()) {
+				return Collections.singletonList(RootEventType.allTypes.indexOf(filter.getEventType()));
+			} else {
+				return filter.getSubFilters().stream().flatMap((Filter t) -> getActiveSubTypes((TypeFilter) t).stream()).collect(Collectors.toList());
+			}
+		} else {
+			return Collections.emptyList();
+		}
+	}
+
+	/**
+	 * get a sqlite strftime format string that will allow us to group by the
+	 * requested period size. That is, with all info more granular than that
+	 * requested dropped (replaced with zeros).
+	 *
+	 * @param timeUnit the {@link TimeUnits} instance describing what
+	 *                 granularity to build a strftime string for
+	 *
+	 * @return a String formatted according to the sqlite strftime spec
+	 *
+	 * @see https://www.sqlite.org/lang_datefunc.html
+	 */
+	static String getStrfTimeFormat(TimeUnits timeUnit) {
+		switch (timeUnit) {
+			case YEARS:
+				return "%Y-01-01T00:00:00"; // NON-NLS
+			case MONTHS:
+				return "%Y-%m-01T00:00:00"; // NON-NLS
+			case DAYS:
+				return "%Y-%m-%dT00:00:00"; // NON-NLS
+			case HOURS:
+				return "%Y-%m-%dT%H:00:00"; // NON-NLS
+			case MINUTES:
+				return "%Y-%m-%dT%H:%M:00"; // NON-NLS
+			case SECONDS:
+			default:    //seconds - should never happen
+				return "%Y-%m-%dT%H:%M:%S"; // NON-NLS  
+		}
+	}
+
+	static String getDescriptionColumn(DescriptionLoD lod) {
+		switch (lod) {
+			case FULL:
+				return "full_description"; //NON-NLS
+			case MEDIUM:
+				return "med_description"; //NON-NLS
+			case SHORT:
+			default:
+				return "short_description"; //NON-NLS
+		}
+	}
+
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/ArtifactEventType.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/ArtifactEventType.java
new file mode 100644
index 0000000000000000000000000000000000000000..e4df415ca0e493d5ed3534b28826afba93a1bbcf
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/ArtifactEventType.java
@@ -0,0 +1,209 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014-16 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.datamodel.timeline;
+
+import java.text.MessageFormat;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.apache.commons.lang3.StringUtils;
+import org.sleuthkit.datamodel.BlackboardArtifact;
+import org.sleuthkit.datamodel.BlackboardAttribute;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/**
+ *
+ */
+public interface ArtifactEventType extends EventType {
+
+    static final Logger LOGGER = Logger.getLogger(ArtifactEventType.class.getName());
+
+    /**
+     * Get the artifact type this event type is derived from.
+     *
+     * @return The artifact type this event type is derived from.
+     */
+    public BlackboardArtifact.Type getArtifactType();
+
+    /**
+     * The attribute type this event type is derived from.
+     *
+     * @return The attribute type this event type is derived from.
+     */
+    public BlackboardAttribute.Type getDateTimeAttributeType();
+
+    /**
+     * Get the ID of the the artifact type that this EventType is derived from.
+     *
+     * @return the ID of the the artifact type that this EventType is derived
+     * from.
+     */
+    public default int getArtifactTypeID() {
+        return getArtifactType().getTypeID();
+    }
+
+    /**
+     * given an artifact, pull out the time stamp, and compose the descriptions.
+     * Each implementation of ArtifactEventType needs to implement
+     * parseAttributesHelper() as hook for
+     * buildEventDescription(org.sleuthkit.datamodel.BlackboardArtifact) to
+     * invoke. Most subtypes can use this default implementation.
+     *
+     * @param artf
+     *
+     * @return an AttributeEventDescription containing the timestamp and
+     * description information
+     *
+     * @throws TskCoreException
+     */
+    default AttributeEventDescription parseAttributesHelper(BlackboardArtifact artf) throws TskCoreException {
+        final BlackboardAttribute dateTimeAttr = artf.getAttribute(getDateTimeAttributeType());
+
+        long time = dateTimeAttr.getValueLong();
+        String shortDescription = getShortExtractor().apply(artf);
+        String medDescription = shortDescription + " : " + getMedExtractor().apply(artf);
+        String fullDescription = medDescription + " : " + getFullExtractor().apply(artf);
+        return new AttributeEventDescription(time, shortDescription, medDescription, fullDescription);
+    }
+
+    /**
+     * @return a function from an artifact to a String to use as part of the
+     * full event description
+     */
+    Function<BlackboardArtifact, String> getFullExtractor();
+
+    /**
+     * @return a function from an artifact to a String to use as part of the
+     * medium event description
+     */
+    Function<BlackboardArtifact, String> getMedExtractor();
+
+    /**
+     * @return a function from an artifact to a String to use as part of the
+     * short event description
+     */
+    Function<BlackboardArtifact, String> getShortExtractor();
+
+    /**
+     * bundles the per event information derived from a BlackBoard Artifact into
+     * one object. Primarily used to have a single return value for
+     * ArtifactEventType#buildEventDescription(ArtifactEventType,
+     * BlackboardArtifact).
+     */
+    static class AttributeEventDescription {
+
+        final private long time;
+
+        public long getTime() {
+            return time;
+        }
+
+        public String getShortDescription() {
+            return shortDescription;
+        }
+
+        public String getMedDescription() {
+            return medDescription;
+        }
+
+        public String getFullDescription() {
+            return fullDescription;
+        }
+
+        final private String shortDescription;
+
+        final private String medDescription;
+
+        final private String fullDescription;
+
+        public AttributeEventDescription(long time, String shortDescription,
+                String medDescription,
+                String fullDescription) {
+            this.time = time;
+            this.shortDescription = shortDescription;
+            this.medDescription = medDescription;
+            this.fullDescription = fullDescription;
+        }
+    }
+
+    /**
+     * Build a AttributeEventDescription derived from a BlackboardArtifact. This
+     * is a template method that relies on each ArtifactEventType's
+     * implementation of ArtifactEventType#parseAttributesHelper() to know how
+     * to go from BlackboardAttributes to the event description.
+     *
+     * @param type
+     * @param artf the BlackboardArtifact to derive the event description from
+     *
+     * @return an AttributeEventDescription derived from the given artifact, if
+     * the given artifact has no timestamp
+     *
+     * @throws TskCoreException is there is a problem accessing the blackboard
+     * data
+     */
+    static public AttributeEventDescription buildEventDescription(ArtifactEventType type, BlackboardArtifact artf) throws TskCoreException {
+        //if we got passed an artifact that doesn't correspond to the type of the event, 
+        //something went very wrong. throw an exception.
+        if (type.getArtifactTypeID() != artf.getArtifactTypeID()) {
+            throw new IllegalArgumentException();
+        }
+        if (artf.getAttribute(type.getDateTimeAttributeType()) == null) {
+            LOGGER.log(Level.WARNING, "Artifact {0} has no date/time attribute, skipping it.", artf.getArtifactID()); // NON-NLS
+            return null;
+        }
+        //use the hook provided by this subtype implementation
+        return type.parseAttributesHelper(artf);
+    }
+
+    static class AttributeExtractor implements Function<BlackboardArtifact, String> {
+
+        public String apply(BlackboardArtifact artf) {
+            return Optional.ofNullable(getAttributeSafe(artf, attributeType))
+                    .map(BlackboardAttribute::getDisplayString)
+                    .map(StringUtils::defaultString)
+                    .orElse("");
+        }
+
+        private final BlackboardAttribute.Type attributeType;
+
+        public AttributeExtractor(BlackboardAttribute.Type attribute) {
+            this.attributeType = attribute;
+        }
+
+    }
+
+    static class EmptyExtractor implements Function<BlackboardArtifact, String> {
+
+        @Override
+        public String apply(BlackboardArtifact t) {
+            return "";
+        }
+    }
+
+    static BlackboardAttribute getAttributeSafe(BlackboardArtifact artf, BlackboardAttribute.Type attrType) {
+        try {
+            return artf.getAttribute(attrType);
+        } catch (TskCoreException ex) {
+            LOGGER.log(Level.SEVERE, MessageFormat.format("Error getting attribute from artifact {0}.", artf.getArtifactID()), ex); // NON-NLS
+            return null;
+        }
+    }
+
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/BaseTypes.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/BaseTypes.java
new file mode 100644
index 0000000000000000000000000000000000000000..085316fe5d831ff18179eb898d3225006520bbd6
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/BaseTypes.java
@@ -0,0 +1,108 @@
+/*
+ * 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.datamodel.timeline;
+
+import java.util.Arrays;
+import java.util.List;
+import javafx.scene.image.Image;
+import static org.sleuthkit.datamodel.timeline.BundleUtils.getBundle;
+
+/**
+ * RootTypes are event types that have no super type.
+ */
+public enum BaseTypes implements EventType {
+	FILE_SYSTEM(getBundle().getString("BaseTypes.fileSystem.name"), "blue-document.png") { // NON-NLS
+
+		@Override
+		public List<? extends EventType> getSubTypes() {
+			return Arrays.asList(FileSystemTypes.values());
+		}
+
+		@Override
+		public EventType getSubType(String string) {
+			return FileSystemTypes.valueOf(string);
+		}
+	},
+	WEB_ACTIVITY(getBundle().getString("BaseTypes.webActivity.name"), "web-file.png") { // NON-NLS
+
+		@Override
+		public List<? extends EventType> getSubTypes() {
+			return Arrays.asList(WebTypes.values());
+		}
+
+		@Override
+		public EventType getSubType(String string) {
+			return WebTypes.valueOf(string);
+		}
+	},
+	MISC_TYPES(getBundle().getString("BaseTypes.miscTypes.name"), "block.png") { // NON-NLS
+
+		@Override
+		public List<? extends EventType> getSubTypes() {
+			return Arrays.asList(MiscTypes.values());
+		}
+
+		@Override
+		public EventType getSubType(String string) {
+			return MiscTypes.valueOf(string);
+		}
+	};
+
+	private final String displayName;
+
+	private final String iconBase;
+
+	private final Image image;
+
+	@Override
+	public Image getFXImage() {
+		return image;
+	}
+
+	@Override
+	public String getIconBase() {
+		return iconBase;
+	}
+
+	@Override
+	public EventTypeZoomLevel getZoomLevel() {
+		return EventTypeZoomLevel.BASE_TYPE;
+	}
+
+	@Override
+	public String getDisplayName() {
+		return displayName;
+	}
+
+	private BaseTypes(String displayName, String iconBase) {
+		this.displayName = displayName;
+		this.iconBase = iconBase;
+		this.image = new Image("org/sleuthkit/autopsy/timeline/images/" + iconBase, true); // NON-NLS
+	}
+
+	@Override
+	public EventType getSuperType() {
+		return RootEventType.getInstance();
+	}
+
+	@Override
+	public EventType getSubType(String string) {
+		return BaseTypes.valueOf(string);
+	}
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/Bundle.properties b/bindings/java/src/org/sleuthkit/datamodel/timeline/Bundle.properties
new file mode 100644
index 0000000000000000000000000000000000000000..bc8eb42086511bf9ee1635ceb28827df64563b9a
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/Bundle.properties
@@ -0,0 +1,31 @@
+
+
+BaseTypes.fileSystem.name=File System
+BaseTypes.webActivity.name=Web Activity
+BaseTypes.miscTypes.name=Misc Types
+FileSystemTypes.fileModified.name=File Modified
+FileSystemTypes.fileAccessed.name=File Accessed
+FileSystemTypes.fileCreated.name=File Created
+FileSystemTypes.fileChanged.name=File Changed
+MiscTypes.message.name=Messages
+MiscTypes.GPSRoutes.name=GPS Routes
+MiscTypes.GPSTrackpoint.name=Location History
+MiscTypes.Calls.name=Calls
+MiscTypes.Email.name=Email
+MiscTypes.recentDocuments.name=Recent Documents
+MiscTypes.installedPrograms.name=Installed Programs
+MiscTypes.exif.name=Exif
+MiscTypes.devicesAttached.name=Devices Attached
+RootEventType.eventTypes.name=Event Types
+WebTypes.webDownloads.name=Web Downloads
+WebTypes.webCookies.name=Web Cookies
+WebTypes.webBookmarks.name=Web Bookmarks
+WebTypes.webHistory.name=Web History
+WebTypes.webSearch.name=Web Searches
+DescriptionLOD.short=Short
+DescriptionLOD.medium=Medium
+DescriptionLOD.full=Full
+
+EventTypeZoomLevel.rootType=Root Type
+EventTypeZoomLevel.baseType=Base Type
+EventTypeZoomLevel.subType=Sub Type
\ No newline at end of file
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/BundleUtils.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/BundleUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..2b8a69b0bd13259c4221ab016966a823e4a5dd1c
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/BundleUtils.java
@@ -0,0 +1,13 @@
+package org.sleuthkit.datamodel.timeline;
+
+
+import java.util.ResourceBundle;
+
+class BundleUtils {
+
+	private static final ResourceBundle BUNDLE = ResourceBundle.getBundle("org.sleuthkit.datamodel.timeline.Bundle");
+
+	static ResourceBundle getBundle() {
+		return BUNDLE;
+	}
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/Bundle_ja.properties b/bindings/java/src/org/sleuthkit/datamodel/timeline/Bundle_ja.properties
new file mode 100644
index 0000000000000000000000000000000000000000..4424dd226be3c747fd624543f09e363fa825067c
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/Bundle_ja.properties
@@ -0,0 +1,34 @@
+
+
+BaseTypes.fileSystem.name=\u30d5\u30a1\u30a4\u30eb\u30b7\u30b9\u30c6\u30e0
+BaseTypes.miscTypes.name=\u305d\u306e\u4ed6\u30bf\u30a4\u30d7
+BaseTypes.webActivity.name=\u30a6\u30a7\u30d6\u30a2\u30af\u30c6\u30a3\u30d3\u30c6\u30a3
+FileSystemTypes.fileAccessed.name=\u30a2\u30af\u30bb\u30b9\u3055\u308c\u305f\u30d5\u30a1\u30a4\u30eb
+FileSystemTypes.fileChanged.name=\u5909\u66f4\u3055\u308c\u305f\u30d5\u30a1\u30a4\u30eb
+FileSystemTypes.fileCreated.name=\u4f5c\u6210\u3055\u308c\u305f\u30d5\u30a1\u30a4\u30eb
+FileSystemTypes.fileModified.name=\u4fee\u6b63\u3055\u308c\u305f\u30d5\u30a1\u30a4\u30eb
+MiscTypes.Calls.name=\u30b3\u30fc\u30eb
+MiscTypes.devicesAttached.name=\u63a5\u7d9a\u3055\u308c\u3066\u3044\u308b\u6a5f\u5668
+MiscTypes.Email.name=Email
+MiscTypes.exif.name=Exif
+MiscTypes.GPSRoutes.name=GPS\u30eb\u30fc\u30c8
+MiscTypes.GPSTrackpoint.name=\u4f4d\u7f6e\u60c5\u5831\u5c65\u6b74
+MiscTypes.installedPrograms.name=\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3055\u308c\u3066\u3044\u308b\u30d7\u30ed\u30b0\u30e9\u30e0
+MiscTypes.message.name=\u30e1\u30c3\u30bb\u30fc\u30b8
+MiscTypes.recentDocuments.name=\u6700\u8fd1\u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8
+RootEventType.eventTypes.name=\u30a4\u30d9\u30f3\u30c8\u30bf\u30a4\u30d7
+WebTypes.webBookmarks.name=\u30a6\u30a7\u30d6\u30d6\u30c3\u30af\u30de\u30fc\u30af
+WebTypes.webCookies.name=\u30a6\u30a7\u30d6\u30af\u30c3\u30ad\u30fc
+WebTypes.webDownloads.name=\u30a6\u30a7\u30d6\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9
+WebTypes.webHistory.name=\u30a6\u30a7\u30d6\u5c65\u6b74
+WebTypes.webSearch.name=\u30a6\u30a7\u30d6\u691c\u7d22
+EventTypeZoomLevel.baseType=\u30d9\u30fc\u30b9\u30bf\u30a4\u30d7
+EventTypeZoomLevel.rootType=\u30eb\u30fc\u30c8\u30bf\u30a4\u30d7
+EventTypeZoomLevel.subType=\u30b5\u30d6\u30bf\u30a4\u30d7
+DescriptionLOD.short=\u7c21\u6f54
+DescriptionLOD.medium=\u6982\u8981
+DescriptionLOD.full=\u8a73\u7d30
+ZoomSettingsPane.descrLODLabel.text=\u8a73\u7d30\u8aac\u660e\uff1a
+ZoomSettingsPane.historyLabel.text=\u5c65\u6b74\uff1a
+ZoomSettingsPane.timeUnitLabel.text=\u6642\u9593\u5358\u4f4d\uff1a
+ZoomSettingsPane.typeZoomLabel.text=\u30a4\u30d9\u30f3\u30c8\u30bf\u30a4\u30d7\uff1a
\ No newline at end of file
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/CombinedEvent.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/CombinedEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..04042006c1b361e0676a6ed90097820ce0f34e60
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/CombinedEvent.java
@@ -0,0 +1,152 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2011-2016 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.datamodel.timeline;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A container for several events that have the same timestamp and description
+ * and are backed by the same file. Used in the ListView to coalesce the file
+ * system events for a file when they have the same timestamp.
+ */
+public class CombinedEvent {
+
+    private final long fileID;
+    private final long epochMillis;
+    private final String description;
+
+    /**
+     * A map from EventType to event ID.
+     */
+    private final Map<EventType, Long> eventTypeMap = new HashMap<EventType, Long>();
+
+    /**
+     * Constructor
+     *
+     * @param epochMillis The timestamp for this event, in millis from the Unix
+     * epoch.
+     * @param description The full description shared by all the combined events
+     * @param fileID The ID of the file shared by all the combined events.
+     * @param eventMap A map from EventType to event ID.
+     */
+    public CombinedEvent(long epochMillis, String description, long fileID, Map<EventType, Long> eventMap) {
+        this.epochMillis = epochMillis;
+        this.description = description;
+        eventTypeMap.putAll(eventMap);
+        this.fileID = fileID;
+    }
+
+    /**
+     * Get the timestamp of this event as millis from the Unix epoch.
+     *
+     * @return The timestamp of this event as millis from the Unix epoch.
+     */
+    public long getStartMillis() {
+        return epochMillis;
+    }
+
+    /**
+     * Get the full description shared by all the combined events.
+     *
+     * @return The full description shared by all the combined events.
+     */
+    public String getDescription() {
+        return description;
+    }
+
+    /**
+     * Get the obj ID of the file shared by the combined events.
+     *
+     * @return The obj ID of the file shared by the combined events.
+     */
+    public long getFileID() {
+        return fileID;
+    }
+
+    /**
+     * Get the types of the combined events.
+     *
+     * @return The types of the combined events.
+     */
+    public Set<EventType> getEventTypes() {
+        return eventTypeMap.keySet();
+    }
+
+    /**
+     * Get the event IDs of the combined events.
+     *
+     * @return The event IDs of the combined events.
+     */
+    public Set<Long> getEventIDs() {
+        return Collections.unmodifiableSet(new HashSet<Long>(eventTypeMap.values()));
+    }
+
+    /**
+     * Get the event ID of one event that is representative of all the combined
+     * events. It can be used to look up a SingleEvent with more details, for
+     * example.
+     *
+     * @return An arbitrary representative event ID for the combined events.
+     */
+    public Long getRepresentativeEventID() {
+        return eventTypeMap.values().stream().findFirst().get();
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 3;
+        hash = 53 * hash + (int) (this.fileID ^ (this.fileID >>> 32));
+        hash = 53 * hash + (int) (this.epochMillis ^ (this.epochMillis >>> 32));
+        hash = 53 * hash + Objects.hashCode(this.description);
+        hash = 53 * hash + Objects.hashCode(this.eventTypeMap);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final CombinedEvent other = (CombinedEvent) obj;
+        if (this.fileID != other.fileID) {
+            return false;
+        }
+        if (this.epochMillis != other.epochMillis) {
+            return false;
+        }
+        if (!Objects.equals(this.description, other.description)) {
+            return false;
+        }
+        if (!Objects.equals(this.eventTypeMap, other.eventTypeMap)) {
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/DescriptionLoD.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/DescriptionLoD.java
new file mode 100644
index 0000000000000000000000000000000000000000..f6171d517ac59febb9904fe3585b4f634d324a9c
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/DescriptionLoD.java
@@ -0,0 +1,87 @@
+/*
+ * 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.datamodel.timeline;
+
+/**
+ * Enumeration of all description levels of detail.
+ */
+public enum DescriptionLoD implements DisplayNameProvider {
+
+    SHORT(BundleUtils.getBundle(). getString("DescriptionLOD.short")),
+    MEDIUM(BundleUtils.getBundle(). getString("DescriptionLOD.medium")),
+    FULL(BundleUtils.getBundle(). getString( "DescriptionLOD.full"));
+
+    private final String displayName;
+
+    @Override
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    private DescriptionLoD(String displayName) {
+        this.displayName = displayName;
+    }
+
+    public DescriptionLoD moreDetailed() {
+        try {
+            return values()[ordinal() + 1];
+        } catch (ArrayIndexOutOfBoundsException e) {
+            return null;
+        }
+    }
+
+    public DescriptionLoD lessDetailed() {
+        try {
+            return values()[ordinal() - 1];
+        } catch (ArrayIndexOutOfBoundsException e) {
+            return null;
+        }
+    }
+
+    public DescriptionLoD withRelativeDetail(RelativeDetail relativeDetail) {
+        switch (relativeDetail) {
+            case EQUAL:
+                return this;
+            case MORE:
+                return moreDetailed();
+            case LESS:
+                return lessDetailed();
+            default:
+                throw new IllegalArgumentException("Unknown RelativeDetail value " + relativeDetail);
+        }
+    }
+
+    public RelativeDetail getDetailLevelRelativeTo(DescriptionLoD other) {
+        int compareTo = this.compareTo(other);
+        if (compareTo < 0) {
+            return RelativeDetail.LESS;
+        } else if (compareTo == 0) {
+            return RelativeDetail.EQUAL;
+        } else {
+            return RelativeDetail.MORE;
+        }
+    }
+
+    public enum RelativeDetail {
+
+        EQUAL,
+        MORE,
+        LESS;
+    }
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/DisplayNameProvider.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/DisplayNameProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..100efb03047dea71f496ec0a226159b91ed58941
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/DisplayNameProvider.java
@@ -0,0 +1,32 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2016 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.datamodel.timeline;
+
+/**
+ * An interface for objects with a display name
+ */
+public interface DisplayNameProvider {
+
+    /**
+     * Get the display name of this object
+     *
+     * @return The display name.
+     */
+    String getDisplayName();
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/EventCluster.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/EventCluster.java
new file mode 100644
index 0000000000000000000000000000000000000000..a2e337c14f89da8d828aababc22a4d90603e2370
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/EventCluster.java
@@ -0,0 +1,252 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2011-2016 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.datamodel.timeline;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Sets;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.SortedSet;
+import org.joda.time.Interval;
+import org.sleuthkit.datamodel.timeline.DescriptionLoD;
+
+/**
+ * Represents a set of other events clustered together. All the sub events
+ * should have the same type and matching descriptions at the designated "zoom
+ * level", and be "close together" in time.
+ */
+public class EventCluster implements MultiEvent<EventStripe> {
+
+    /**
+     * merge two event clusters into one new event cluster.
+     *
+     * @param cluster1
+     * @param cluster2
+     *
+     * @return a new event cluster that is the result of merging the given
+     *         events clusters
+     */
+    public static EventCluster merge(EventCluster cluster1, EventCluster cluster2) {
+        if (cluster1.getEventType() != cluster2.getEventType()) {
+            throw new IllegalArgumentException("event clusters are not compatible: they have different types");
+        }
+
+        if (!cluster1.getDescription().equals(cluster2.getDescription())) {
+            throw new IllegalArgumentException("event clusters are not compatible: they have different descriptions");
+        }
+        Sets.SetView<Long> idsUnion =
+                Sets.union(cluster1.getEventIDs(), cluster2.getEventIDs());
+        Sets.SetView<Long> hashHitsUnion =
+                Sets.union(cluster1.getEventIDsWithHashHits(), cluster2.getEventIDsWithHashHits());
+        Sets.SetView<Long> taggedUnion =
+                Sets.union(cluster1.getEventIDsWithTags(), cluster2.getEventIDsWithTags());
+
+        return new EventCluster(IntervalUtils.span(cluster1.span, cluster2.span),
+                cluster1.getEventType(), idsUnion, hashHitsUnion, taggedUnion,
+                cluster1.getDescription(), cluster1.lod);
+    }
+
+    final private EventStripe parent;
+
+    /**
+     * the smallest time interval containing all the clustered events
+     */
+    final private Interval span;
+
+    /**
+     * the type of all the clustered events
+     */
+    final private EventType type;
+
+    /**
+     * the common description of all the clustered events
+     */
+    final private String description;
+
+    /**
+     * the description level of detail that the events were clustered at.
+     */
+    private final DescriptionLoD lod;
+
+    /**
+     * the set of ids of the clustered events
+     */
+    final private ImmutableSet<Long> eventIDs;
+
+    /**
+     * the ids of the subset of clustered events that have at least one tag
+     * applied to them
+     */
+    private final ImmutableSet<Long> tagged;
+
+    /**
+     * the ids of the subset of clustered events that have at least one hash set
+     * hit
+     */
+    private final ImmutableSet<Long> hashHits;
+
+    private EventCluster(Interval spanningInterval, EventType type, Collection<Long> eventIDs,
+            Collection<Long> hashHits, Collection<Long> tagged, String description, DescriptionLoD lod,
+            EventStripe parent) {
+
+        this.span = spanningInterval;
+        this.type = type;
+        this.hashHits = ImmutableSet.copyOf(hashHits);
+        this.tagged = ImmutableSet.copyOf(tagged);
+        this.description = description;
+        this.eventIDs = ImmutableSet.copyOf(eventIDs);
+        this.lod = lod;
+        this.parent = parent;
+    }
+
+    public EventCluster(Interval spanningInterval, EventType type, Collection<Long> eventIDs,
+            Collection<Long> hashHits, Collection<Long> tagged, String description, DescriptionLoD lod) {
+        this(spanningInterval, type, eventIDs, hashHits, tagged, description, lod, null);
+    }
+
+    /**
+     * get the EventStripe (if any) that contains this cluster
+     *
+     * @return an Optional containg the parent stripe of this cluster, or is
+     *         empty if the cluster has no parent set.
+     */
+    @Override
+    public Optional<EventStripe> getParent() {
+        return Optional.ofNullable(parent);
+    }
+
+    /**
+     * get the EventStripe (if any) that contains this cluster
+     *
+     * @return an Optional containg the parent stripe of this cluster, or is
+     *         empty if the cluster has no parent set.
+     */
+    @Override
+    public Optional<EventStripe> getParentStripe() {
+        //since this clusters parent must be an event stripe just delegate to getParent();
+        return getParent();
+    }
+
+    public Interval getSpan() {
+        return span;
+    }
+
+    @Override
+    public long getStartMillis() {
+        return span.getStartMillis();
+    }
+
+    @Override
+    public long getEndMillis() {
+        return span.getEndMillis();
+    }
+
+    @Override
+    public ImmutableSet<Long> getEventIDs() {
+        return eventIDs;
+    }
+
+    @Override
+    public ImmutableSet<Long> getEventIDsWithHashHits() {
+        return hashHits;
+    }
+
+    @Override
+    public ImmutableSet<Long> getEventIDsWithTags() {
+        return tagged;
+    }
+
+    @Override
+    public String getDescription() {
+        return description;
+    }
+
+    @Override
+    public EventType getEventType() {
+        return type;
+    }
+
+    @Override
+    public DescriptionLoD getDescriptionLoD() {
+        return lod;
+    }
+
+    /**
+     * return a new EventCluster identical to this one, except with the given
+     * EventBundle as the parent.
+     *
+     * @param parent
+     *
+     * @return a new EventCluster identical to this one, except with the given
+     *         EventBundle as the parent.
+     */
+    public EventCluster withParent(EventStripe parent) {
+        return new EventCluster(span, type, eventIDs, hashHits, tagged, description, lod, parent);
+    }
+
+    @Override
+    public SortedSet<EventCluster> getClusters() {
+        return ImmutableSortedSet.orderedBy(Comparator.comparing(EventCluster::getStartMillis)).add(this).build();
+    }
+
+    @Override
+    public String toString() {
+        return "EventCluster{" + "description=" + description + ", eventIDs=" + eventIDs.size() + '}';
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        hash = 23 * hash + Objects.hashCode(this.type);
+        hash = 23 * hash + Objects.hashCode(this.description);
+        hash = 23 * hash + Objects.hashCode(this.lod);
+        hash = 23 * hash + Objects.hashCode(this.eventIDs);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final EventCluster other = (EventCluster) obj;
+        if (!Objects.equals(this.description, other.description)) {
+            return false;
+        }
+        if (!Objects.equals(this.type, other.type)) {
+            return false;
+        }
+        if (this.lod != other.lod) {
+            return false;
+        }
+        if (!Objects.equals(this.eventIDs, other.eventIDs)) {
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/EventStripe.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/EventStripe.java
new file mode 100644
index 0000000000000000000000000000000000000000..cb448e31e72d8b248f83e5fcb837a5c2c18a8b0b
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/EventStripe.java
@@ -0,0 +1,241 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2015-16 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.datamodel.timeline;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import java.util.Comparator;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.SortedSet;
+import org.sleuthkit.datamodel.timeline.DescriptionLoD;
+
+/**
+ * A 'collection' of {@link EventCluster}s, all having the same type,
+ * description, and zoom levels, but not necessarily close together in time.
+ */
+public final class EventStripe implements MultiEvent<EventCluster> {
+
+    public static EventStripe merge(EventStripe u, EventStripe v) {
+        Preconditions.checkNotNull(u);
+        Preconditions.checkNotNull(v);
+        Preconditions.checkArgument(Objects.equals(u.description, v.description));
+        Preconditions.checkArgument(Objects.equals(u.lod, v.lod));
+        Preconditions.checkArgument(Objects.equals(u.type, v.type));
+        Preconditions.checkArgument(Objects.equals(u.parent, v.parent));
+        return new EventStripe(u, v);
+    }
+
+    private final EventCluster parent;
+
+    private final ImmutableSortedSet<EventCluster> clusters;
+
+    /**
+     * the type of all the events
+     */
+    private final EventType type;
+
+    /**
+     * the common description of all the events
+     */
+    private final String description;
+
+    /**
+     * the description level of detail that the events were clustered at.
+     */
+    private final DescriptionLoD lod;
+
+    /**
+     * the set of ids of the events
+     */
+    private final ImmutableSet<Long> eventIDs;
+
+    /**
+     * the ids of the subset of events that have at least one tag applied to
+     * them
+     */
+    private final ImmutableSet<Long> tagged;
+
+    /**
+     * the ids of the subset of events that have at least one hash set hit
+     */
+    private final ImmutableSet<Long> hashHits;
+
+    public EventStripe withParent(EventCluster parent) {
+        if (java.util.Objects.nonNull(this.parent)) {
+            throw new IllegalStateException("Event Stripe already has a parent!");
+        }
+        return new EventStripe(parent, this.type, this.description, this.lod, clusters, eventIDs, tagged, hashHits);
+    }
+
+    private EventStripe(EventCluster parent, EventType type, String description, DescriptionLoD lod, SortedSet<EventCluster> clusters, ImmutableSet<Long> eventIDs, ImmutableSet<Long> tagged, ImmutableSet<Long> hashHits) {
+        this.parent = parent;
+        this.type = type;
+        this.description = description;
+        this.lod = lod;
+        this.clusters = ImmutableSortedSet.copyOf(Comparator.comparing(EventCluster::getStartMillis), clusters);
+
+        this.eventIDs = eventIDs;
+        this.tagged = tagged;
+        this.hashHits = hashHits;
+    }
+
+    public EventStripe(EventCluster cluster) {
+
+        this.clusters = ImmutableSortedSet.orderedBy(Comparator.comparing(EventCluster::getStartMillis))
+                .add(cluster.withParent(this)).build();
+
+        type = cluster.getEventType();
+        description = cluster.getDescription();
+        lod = cluster.getDescriptionLoD();
+        eventIDs = cluster.getEventIDs();
+        tagged = cluster.getEventIDsWithTags();
+        hashHits = cluster.getEventIDsWithHashHits();
+        this.parent = null;
+    }
+
+    private EventStripe(EventStripe u, EventStripe v) {
+        clusters = ImmutableSortedSet.orderedBy(Comparator.comparing(EventCluster::getStartMillis))
+                .addAll(u.getClusters())
+                .addAll(v.getClusters())
+                .build();
+
+        type = u.getEventType();
+        description = u.getDescription();
+        lod = u.getDescriptionLoD();
+        eventIDs = ImmutableSet.<Long>builder()
+                .addAll(u.getEventIDs())
+                .addAll(v.getEventIDs())
+                .build();
+        tagged = ImmutableSet.<Long>builder()
+                .addAll(u.getEventIDsWithTags())
+                .addAll(v.getEventIDsWithTags())
+                .build();
+        hashHits = ImmutableSet.<Long>builder()
+                .addAll(u.getEventIDsWithHashHits())
+                .addAll(v.getEventIDsWithHashHits())
+                .build();
+        parent = u.getParent().orElse(v.getParent().orElse(null));
+    }
+
+    @Override
+    public Optional<EventCluster> getParent() {
+        return Optional.ofNullable(parent);
+    }
+
+    public Optional<EventStripe> getParentStripe() {
+        if (getParent().isPresent()) {
+            return getParent().get().getParent();
+        } else {
+            return Optional.empty();
+        }
+    }
+
+    @Override
+    public String getDescription() {
+        return description;
+    }
+
+    @Override
+    public EventType getEventType() {
+        return type;
+    }
+
+    @Override
+    public DescriptionLoD getDescriptionLoD() {
+        return lod;
+    }
+
+    @Override
+    public ImmutableSet<Long> getEventIDs() {
+        return eventIDs;
+    }
+
+    @Override
+    public ImmutableSet<Long> getEventIDsWithHashHits() {
+        return hashHits;
+    }
+
+    @Override
+    public ImmutableSet<Long> getEventIDsWithTags() {
+        return tagged;
+    }
+
+    @Override
+    public long getStartMillis() {
+        return clusters.first().getStartMillis();
+    }
+
+    @Override
+    public long getEndMillis() {
+        return clusters.last().getEndMillis();
+    }
+
+    @Override
+    public ImmutableSortedSet< EventCluster> getClusters() {
+        return clusters;
+    }
+
+    @Override
+    public String toString() {
+        return "EventStripe{" + "description=" + description + ", eventIDs=" + (Objects.isNull(eventIDs) ? 0 : eventIDs.size()) + '}'; //NON-NLS
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 3;
+        hash = 79 * hash + Objects.hashCode(this.clusters);
+        hash = 79 * hash + Objects.hashCode(this.type);
+        hash = 79 * hash + Objects.hashCode(this.description);
+        hash = 79 * hash + Objects.hashCode(this.lod);
+        hash = 79 * hash + Objects.hashCode(this.eventIDs);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final EventStripe other = (EventStripe) obj;
+        if (!Objects.equals(this.description, other.description)) {
+            return false;
+        }
+        if (!Objects.equals(this.clusters, other.clusters)) {
+            return false;
+        }
+        if (!Objects.equals(this.type, other.type)) {
+            return false;
+        }
+        if (this.lod != other.lod) {
+            return false;
+        }
+        if (!Objects.equals(this.eventIDs, other.eventIDs)) {
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/EventType.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/EventType.java
new file mode 100644
index 0000000000000000000000000000000000000000..c5d92b4b44a1ff730f3342559fbec265172f8bff
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/EventType.java
@@ -0,0 +1,109 @@
+/*
+ * 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.datamodel.timeline;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import javafx.scene.image.Image;
+import javafx.scene.paint.Color;
+
+/**
+ * An Event Type represents a distinct kind of event ie file system or web
+ * activity. An EventType may have an optional super-type and 0 or more
+ * subtypes, allowing events to be organized in a type hierarchy.
+ */
+public interface EventType {
+
+    final static List<? extends EventType> allTypes = RootEventType.getInstance().getSubTypesRecusive();
+
+    static Comparator<EventType> getComparator() {
+        return Comparator.comparing(EventType.allTypes::indexOf);
+
+    }
+
+    default BaseTypes getBaseType() {
+        if (this instanceof BaseTypes) {
+            return (BaseTypes) this;
+        } else {
+            return getSuperType().getBaseType();
+        }
+    }
+
+    default List<? extends EventType> getSubTypesRecusive() {
+        ArrayList<EventType> flatList = new ArrayList<>();
+
+        for (EventType et : getSubTypes()) {
+            flatList.add(et);
+            flatList.addAll(et.getSubTypesRecusive());
+        }
+        return flatList;
+    }
+
+    /**
+     * @return the color used to represent this event type visually
+     */
+    default Color getColor() {
+
+        Color baseColor = this.getSuperType().getColor();
+        int siblings = getSuperType().getSiblingTypes().stream().max((
+                EventType t, EventType t1)
+                -> Integer.compare(t.getSubTypes().size(), t1.getSubTypes().size()))
+                .get().getSubTypes().size() + 1;
+        int superSiblings = this.getSuperType().getSiblingTypes().size();
+
+        double offset = (360.0 / superSiblings) / siblings;
+        final Color deriveColor = baseColor.deriveColor(ordinal() * offset, 1, 1, 1);
+
+        return Color.hsb(deriveColor.getHue(), deriveColor.getSaturation(), deriveColor.getBrightness());
+
+    }
+
+    default List<? extends EventType> getSiblingTypes() {
+        return this.getSuperType().getSubTypes();
+    }
+
+    /**
+     * @return the super type of this event
+     */
+    public EventType getSuperType();
+
+    public EventTypeZoomLevel getZoomLevel();
+
+    /**
+     * @return a list of event types, one for each subtype of this eventype, or
+     *         an empty list if this event type has no subtypes
+     */
+    public List<? extends EventType> getSubTypes();
+
+    /*
+     * return the name of the icon file for this type, it will be resolved in
+     * the org/sleuthkit/autopsy/timeline/images
+     */
+    public String getIconBase();
+
+    public String getDisplayName();
+
+    public EventType getSubType(String string);
+
+    public Image getFXImage();
+
+    public int ordinal();
+
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/EventTypeZoomLevel.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/EventTypeZoomLevel.java
new file mode 100644
index 0000000000000000000000000000000000000000..d5659fad6dcea6404e2b2f44e6c684c3bd980da2
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/EventTypeZoomLevel.java
@@ -0,0 +1,41 @@
+/*
+ * 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.datamodel.timeline;
+
+
+/**
+ * Enum of event type zoom levels
+ */
+public enum EventTypeZoomLevel implements DisplayNameProvider {
+
+    ROOT_TYPE(BundleUtils.getBundle().getString("EventTypeZoomLevel.rootType")),
+    BASE_TYPE(BundleUtils.getBundle().getString("EventTypeZoomLevel.baseType")),
+    SUB_TYPE(BundleUtils.getBundle().getString("EventTypeZoomLevel.subType"));
+
+    @Override
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    private final String displayName;
+
+    private EventTypeZoomLevel(String displayName) {
+        this.displayName = displayName;
+    }
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/FileSystemTypes.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/FileSystemTypes.java
new file mode 100644
index 0000000000000000000000000000000000000000..918e6aeec6471eb7a90f6a3804630e11ef276240
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/FileSystemTypes.java
@@ -0,0 +1,81 @@
+/*
+ * 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.datamodel.timeline;
+
+import java.util.Collections;
+import java.util.List;
+import javafx.scene.image.Image;
+
+/**
+ *
+ */
+public enum FileSystemTypes implements EventType {
+
+    FILE_MODIFIED(BundleUtils.getBundle().getString("FileSystemTypes.fileModified.name"), "blue-document-attribute-m.png"), // NON-NLS
+    FILE_ACCESSED(BundleUtils.getBundle().getString("FileSystemTypes.fileAccessed.name"), "blue-document-attribute-a.png"), // NON-NLS
+    FILE_CREATED(BundleUtils.getBundle().getString("FileSystemTypes.fileCreated.name"), "blue-document-attribute-b.png"), // NON-NLS
+    FILE_CHANGED(BundleUtils.getBundle().getString("FileSystemTypes.fileChanged.name"), "blue-document-attribute-c.png"); // NON-NLS
+
+    private final String iconBase;
+
+    private final Image image;
+
+    @Override
+    public Image getFXImage() {
+        return image;
+    }
+
+    @Override
+    public String getIconBase() {
+        return iconBase;
+    }
+
+    @Override
+    public EventTypeZoomLevel getZoomLevel() {
+        return EventTypeZoomLevel.SUB_TYPE;
+    }
+
+    private final String displayName;
+
+    @Override
+    public EventType getSubType(String string) {
+        return FileSystemTypes.valueOf(string);
+    }
+
+    @Override
+    public EventType getSuperType() {
+        return BaseTypes.FILE_SYSTEM;
+    }
+
+    @Override
+    public List<? extends EventType> getSubTypes() {
+        return Collections.emptyList();
+    }
+
+    private FileSystemTypes(String displayName, String iconBase) {
+        this.displayName = displayName;
+        this.iconBase = iconBase;
+        this.image = new Image("org/sleuthkit/autopsy/timeline/images/" + iconBase, true); // NON-NLS
+    }
+
+    @Override
+    public String getDisplayName() {
+        return displayName;
+    }
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/IntervalUtils.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/IntervalUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..e7a78f789736d95be87910189461b9780de3a9ea
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/IntervalUtils.java
@@ -0,0 +1,93 @@
+/*
+ * 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.datamodel.timeline;
+
+import java.time.Instant;
+import java.time.temporal.TemporalAmount;
+import java.util.Collection;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.Interval;
+import org.joda.time.ReadablePeriod;
+
+/**
+ *
+ */
+public class IntervalUtils {
+
+    static public Interval getSpanningInterval(Collection<DateTime> times) {
+        Interval trange = null;
+        for (DateTime t : times) {
+            if (trange == null) {
+                trange = new Interval(t.getMillis(), t.getMillis() + 1000, DateTimeZone.UTC);
+            } else {
+                trange = extendInterval(trange, t.getMillis());
+            }
+        }
+        return trange;
+    }
+
+    static public Interval span(Interval range, final Interval range2) {
+        return new Interval(Math.min(range.getStartMillis(), range2.getStartMillis()), Math.max(range.getEndMillis(), range2.getEndMillis()), DateTimeZone.UTC);
+    }
+
+    static public Interval extendInterval(Interval range, final Long eventTime) {
+        return new Interval(Math.min(range.getStartMillis(), eventTime), Math.max(range.getEndMillis(), eventTime + 1), DateTimeZone.UTC);
+    }
+
+    public static DateTime middleOf(Interval interval) {
+        return new DateTime((interval.getStartMillis() + interval.getEndMillis()) / 2);
+    }
+
+    public static Interval getAdjustedInterval(Interval oldInterval, TimeUnits requestedUnit) {
+        return getIntervalAround(middleOf(oldInterval), requestedUnit.getPeriod());
+    }
+
+    static public Interval getIntervalAround(DateTime aroundInstant, ReadablePeriod period) {
+        DateTime start = aroundInstant.minus(period);
+        DateTime end = aroundInstant.plus(period);
+        Interval range = new Interval(start, end);
+        DateTime middleOf = IntervalUtils.middleOf(range);
+        long halfRange = range.toDurationMillis() / 4;
+        final Interval newInterval = new Interval(middleOf.minus(halfRange), middleOf.plus(halfRange));
+        return newInterval;
+    }
+
+    static public Interval getIntervalAround(Instant aroundInstant, TemporalAmount temporalAmount) {
+        long start = aroundInstant.minus(temporalAmount).toEpochMilli();
+        long end = aroundInstant.plusMillis(1).plus(temporalAmount).toEpochMilli();
+        final Interval newInterval = new Interval(start, Math.max(start + 1, end));
+        return newInterval;
+    }
+
+    /**
+     * Get an interval the length of the given period, centered around the
+     * center of the given interval.
+     *
+     * @param interval The interval whose center will be the center of the new
+     * interval.
+     * @param period The length of the new interval
+     *
+     * @return An interval the length of the given period, centered around the
+     * center of the given interval.
+     */
+    static public Interval getIntervalAroundMiddle(Interval interval, ReadablePeriod period) {
+        return getIntervalAround(middleOf(interval), period);
+    }
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/MiscTypes.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/MiscTypes.java
new file mode 100644
index 0000000000000000000000000000000000000000..51762116c2981f0e1cdba8b28be033be81065a3c
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/MiscTypes.java
@@ -0,0 +1,262 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014-16 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.datamodel.timeline;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.logging.Level;
+import javafx.scene.image.Image;
+import org.apache.commons.lang3.StringUtils;
+import org.sleuthkit.datamodel.AbstractFile;
+import org.sleuthkit.datamodel.BlackboardArtifact;
+import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE;
+import org.sleuthkit.datamodel.BlackboardAttribute;
+import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE;
+import org.sleuthkit.datamodel.TskCoreException;
+import org.sleuthkit.datamodel.timeline.ArtifactEventType.AttributeEventDescription;
+import org.sleuthkit.datamodel.timeline.ArtifactEventType.AttributeExtractor;
+import org.sleuthkit.datamodel.timeline.ArtifactEventType.EmptyExtractor;
+import static org.sleuthkit.datamodel.timeline.ArtifactEventType.LOGGER;
+import static org.sleuthkit.datamodel.timeline.ArtifactEventType.getAttributeSafe;
+
+/**
+ *
+ */
+public enum MiscTypes implements EventType, ArtifactEventType {
+
+    MESSAGE(BundleUtils.getBundle().getString( "MiscTypes.message.name"), "message.png", // NON-NLS
+            new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_MESSAGE),
+            new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DATETIME),
+            new AttributeExtractor(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_MESSAGE_TYPE)),
+            artf -> {
+                final BlackboardAttribute dir = getAttributeSafe(artf, new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DIRECTION));
+                final BlackboardAttribute readStatus = getAttributeSafe(artf, new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_READ_STATUS));
+                final BlackboardAttribute name = getAttributeSafe(artf, new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_NAME));
+                final BlackboardAttribute phoneNumber = getAttributeSafe(artf, new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_PHONE_NUMBER));
+                final BlackboardAttribute subject = getAttributeSafe(artf, new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_SUBJECT));
+                List<String> asList = Arrays.asList(stringValueOf(dir), stringValueOf(readStatus), name != null || phoneNumber != null ? toFrom(dir) : "", stringValueOf(name != null ? name : phoneNumber), (subject == null ? "" : stringValueOf(subject)));
+                return StringUtils.join(asList, " ");
+            },
+            new AttributeExtractor(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_TEXT))),
+    GPS_ROUTE(BundleUtils.getBundle().getString( "MiscTypes.GPSRoutes.name"), "gps-search.png", // NON-NLS
+            new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_GPS_ROUTE),
+            new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DATETIME),
+            new AttributeExtractor(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_PROG_NAME)),
+            new AttributeExtractor(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_LOCATION)),
+            artf -> {
+                final BlackboardAttribute latStart = getAttributeSafe(artf, new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_START));
+                final BlackboardAttribute longStart = getAttributeSafe(artf, new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_START));
+                final BlackboardAttribute latEnd = getAttributeSafe(artf, new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_END));
+                final BlackboardAttribute longEnd = getAttributeSafe(artf, new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_END));
+                return String.format("from %1$s %2$s to %3$s %4$s", stringValueOf(latStart), stringValueOf(longStart), stringValueOf(latEnd), stringValueOf(longEnd)); // NON-NLS
+            }),
+    GPS_TRACKPOINT(BundleUtils.getBundle().getString( "MiscTypes.GPSTrackpoint.name"), "gps-trackpoint.png", // NON-NLS
+            new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_GPS_TRACKPOINT),
+            new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DATETIME),
+            new AttributeExtractor(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_PROG_NAME)),
+            artf -> {
+                final BlackboardAttribute longitude = getAttributeSafe(artf, new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE));
+                final BlackboardAttribute latitude = getAttributeSafe(artf, new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_GEO_LATITUDE));
+                return stringValueOf(latitude) + " " + stringValueOf(longitude); // NON-NLS
+            },
+            new EmptyExtractor()),
+    CALL_LOG(BundleUtils.getBundle().getString( "MiscTypes.Calls.name"), "calllog.png", // NON-NLS
+            new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_CALLLOG),
+            new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DATETIME_START),
+            new AttributeExtractor(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_NAME)),
+            new AttributeExtractor(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_PHONE_NUMBER)),
+            new AttributeExtractor(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DIRECTION))),
+    EMAIL(BundleUtils.getBundle().getString( "MiscTypes.Email.name"), "mail-icon-16.png", // NON-NLS
+            new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_EMAIL_MSG),
+            new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DATETIME_SENT),
+            artf -> {
+                final BlackboardAttribute emailFrom = getAttributeSafe(artf, new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_EMAIL_FROM));
+                final BlackboardAttribute emailTo = getAttributeSafe(artf, new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_EMAIL_TO));
+                return stringValueOf(emailFrom) + " to " + stringValueOf(emailTo); // NON-NLS
+            },
+            new AttributeExtractor(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_SUBJECT)),
+            new AttributeExtractor(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_EMAIL_CONTENT_PLAIN))),
+    RECENT_DOCUMENTS(BundleUtils.getBundle().getString( "MiscTypes.recentDocuments.name"), "recent_docs.png", // NON-NLS
+            new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_RECENT_OBJECT),
+            new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DATETIME),
+            new AttributeExtractor(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_PATH)).andThen(
+                    (String t) -> (StringUtils.substringBeforeLast(StringUtils.substringBeforeLast(t, "\\"), "\\"))),
+            new AttributeExtractor(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_PATH)).andThen(
+                    (String t) -> StringUtils.substringBeforeLast(t, "\\")),
+            new AttributeExtractor(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_PATH))) {
+
+                @Override
+        public AttributeEventDescription parseAttributesHelper(BlackboardArtifact artf) throws TskCoreException {
+            final BlackboardAttribute dateTimeAttr = artf.getAttribute(getDateTimeAttributeType());
+
+                    long time = dateTimeAttr.getValueLong();
+
+                    //Non-default description construction
+                    String shortDescription = getShortExtractor().apply(artf);
+                    String medDescription = getMedExtractor().apply(artf);
+                    String fullDescription = getFullExtractor().apply(artf);
+
+                    return new AttributeEventDescription(time, shortDescription, medDescription, fullDescription);
+                }
+            },
+    INSTALLED_PROGRAM(BundleUtils.getBundle().getString( "MiscTypes.installedPrograms.name"), "programs.png", // NON-NLS
+            new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_INSTALLED_PROG),
+            new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DATETIME),
+            new AttributeExtractor(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_PROG_NAME)),
+            new EmptyExtractor(),
+            new EmptyExtractor()),
+    EXIF(BundleUtils.getBundle().getString( "MiscTypes.exif.name"), "camera-icon-16.png", // NON-NLS
+            new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_METADATA_EXIF),
+            new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DATETIME_CREATED),
+            new AttributeExtractor(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DEVICE_MAKE)),
+            new AttributeExtractor(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DEVICE_MODEL)),
+            artf -> {
+                try {
+                    AbstractFile file = artf.getSleuthkitCase().getAbstractFileById(artf.getObjectID());
+                    if (file != null) {
+                        return file.getName();
+                    }
+                } catch (TskCoreException ex) {
+                    LOGGER.log(Level.SEVERE, "Exif event type failed to look up backing file name", ex); //NON-NLS
+                }
+                return "error loading file name";
+            }),
+    DEVICES_ATTACHED(BundleUtils.getBundle().getString( "MiscTypes.devicesAttached.name"), "usb_devices.png", // NON-NLS
+            new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_DEVICE_ATTACHED),
+            new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DATETIME),
+            new AttributeExtractor(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DEVICE_MAKE)),
+            new AttributeExtractor(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DEVICE_MODEL)),
+            new AttributeExtractor(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DEVICE_ID)));
+
+    static public String stringValueOf(BlackboardAttribute attr) {
+        return Optional.ofNullable(attr)
+                .map(BlackboardAttribute::getDisplayString)
+                .orElse("");
+    }
+
+    public static String toFrom(BlackboardAttribute dir) {
+        if (dir == null) {
+            return "";
+        } else {
+            switch (dir.getDisplayString()) {
+                case "Incoming": // NON-NLS
+                    return "from"; // NON-NLS
+                case "Outgoing": // NON-NLS
+                    return "to"; // NON-NLS
+                default:
+                    return ""; // NON-NLS
+            }
+        }
+    }
+
+    private final BlackboardAttribute.Type dateTimeAttributeType;
+
+    private final String iconBase;
+
+    private final Image image;
+
+    @Override
+    public Image getFXImage() {
+        return image;
+    }
+
+    private final Function<BlackboardArtifact, String> longExtractor;
+
+    private final Function<BlackboardArtifact, String> medExtractor;
+
+    private final Function<BlackboardArtifact, String> shortExtractor;
+
+    @Override
+    public Function<BlackboardArtifact, String> getFullExtractor() {
+        return longExtractor;
+    }
+
+    @Override
+    public Function<BlackboardArtifact, String> getMedExtractor() {
+        return medExtractor;
+    }
+
+    @Override
+    public Function<BlackboardArtifact, String> getShortExtractor() {
+        return shortExtractor;
+    }
+
+    @Override
+    public BlackboardAttribute.Type getDateTimeAttributeType() {
+        return dateTimeAttributeType;
+    }
+
+    @Override
+    public EventTypeZoomLevel getZoomLevel() {
+        return EventTypeZoomLevel.SUB_TYPE;
+    }
+
+    private final String displayName;
+
+    private final BlackboardArtifact.Type artifactType;
+
+    @Override
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    @Override
+    public String getIconBase() {
+        return iconBase;
+    }
+
+    @Override
+    public EventType getSubType(String string) {
+        return MiscTypes.valueOf(string);
+    }
+
+    private MiscTypes(String displayName, String iconBase, BlackboardArtifact.Type artifactType,
+            BlackboardAttribute.Type dateTimeAttributeType,
+            Function<BlackboardArtifact, String> shortExtractor,
+            Function<BlackboardArtifact, String> medExtractor,
+            Function<BlackboardArtifact, String> longExtractor) {
+        this.displayName = displayName;
+        this.iconBase = iconBase;
+        this.artifactType = artifactType;
+        this.dateTimeAttributeType = dateTimeAttributeType;
+        this.shortExtractor = shortExtractor;
+        this.medExtractor = medExtractor;
+        this.longExtractor = longExtractor;
+        this.image = new Image("org/sleuthkit/autopsy/timeline/images/" + iconBase, true); // NON-NLS
+    }
+
+    @Override
+    public EventType getSuperType() {
+        return BaseTypes.MISC_TYPES;
+    }
+
+    @Override
+    public List<? extends EventType> getSubTypes() {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public BlackboardArtifact.Type getArtifactType() {
+        return artifactType;
+    }
+
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/MultiEvent.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/MultiEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..792723226886d670a18a8f3643a7cbe9a848f11e
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/MultiEvent.java
@@ -0,0 +1,37 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2015-16 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.datamodel.timeline;
+
+import java.util.Optional;
+import java.util.SortedSet;
+
+/**
+ * A interface for groups of events that share some attributes in common.
+ * @param <ParentType>
+ */
+public interface MultiEvent<ParentType extends MultiEvent<?>> extends TimeLineEvent {
+
+    @Override
+    long getEndMillis();
+
+    Optional<ParentType> getParent();
+
+    @Override
+    SortedSet<EventCluster> getClusters();
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/RangeDivisionInfo.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/RangeDivisionInfo.java
new file mode 100644
index 0000000000000000000000000000000000000000..7d3928533715a2bb03fbcef66d4f05e034aaf700
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/RangeDivisionInfo.java
@@ -0,0 +1,189 @@
+/*
+ * 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.datamodel.timeline;
+
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+import java.util.List;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeFieldType;
+import org.joda.time.DateTimeZone;
+import org.joda.time.Days;
+import org.joda.time.Hours;
+import org.joda.time.Interval;
+import org.joda.time.Minutes;
+import org.joda.time.Months;
+import org.joda.time.Seconds;
+import org.joda.time.Years;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.ISODateTimeFormat;
+
+/**
+ * Bundles up the results of analyzing a time range for the appropriate
+ * TimeUnits to use to visualize it. Partly, this class exists so I
+ * don't have to have more member variables in other places , and partly because
+ * I can only return a single value from a function. This might only be a
+ * temporary design but is working well for now.
+ */
+public class RangeDivisionInfo {
+
+    /**
+     * the size of the periods we should divide the interval into
+     */
+    private final TimeUnits blockSize;
+
+    /**
+     * The number of Blocks we are going to divide the interval into.
+     */
+    private final int numberOfBlocks;
+
+    /**
+     * a DateTimeFormatter corresponding to the block size for the tick
+     * marks on the date axis of the graph
+     */
+    private final DateTimeFormatter tickFormatter;
+
+    /**
+     * an adjusted lower bound for the range such that is lines up with a block
+     * boundary before or at the start of the timerange
+     */
+    private final long lowerBound;
+
+    /**
+     * an adjusted upper bound for the range such that is lines up with a block
+     * boundary at or after the end of the timerange
+     */
+    private final long upperBound;
+
+    /**
+     * the time range this RangeDivisionInfo describes
+     */
+    private final Interval timeRange;
+    private ImmutableList<Interval> intervals;
+
+    public Interval getTimeRange() {
+        return timeRange;
+    }
+
+    private RangeDivisionInfo(Interval timeRange, int periodsInRange, TimeUnits periodSize, DateTimeFormatter tickformatter, long lowerBound, long upperBound) {
+        this.numberOfBlocks = periodsInRange;
+        this.blockSize = periodSize;
+        this.tickFormatter = tickformatter;
+
+        this.lowerBound = lowerBound;
+        this.upperBound = upperBound;
+        this.timeRange = timeRange;
+    }
+
+    /**
+     * Static factory method.
+     *
+     * Determine the period size, number of periods, whole period bounds, and
+     * formatters to use to visualize the given timerange.
+     *
+     * @param timeRange
+     *
+     * @return
+     */
+    public static RangeDivisionInfo getRangeDivisionInfo(Interval timeRange, DateTimeZone tz) {
+        //Check from largest to smallest unit
+
+        //TODO: make this more generic... reduce code duplication -jm
+        DateTimeFieldType timeUnit;
+        final DateTime startWithZone = timeRange.getStart().withZone(tz);
+        final DateTime endWithZone = timeRange.getEnd().withZone(tz);
+
+        if (Years.yearsIn(timeRange).isGreaterThan(Years.THREE)) {
+            timeUnit = DateTimeFieldType.year();
+            long lower = startWithZone.property(timeUnit).roundFloorCopy().getMillis();
+            long upper = endWithZone.property(timeUnit).roundCeilingCopy().getMillis();
+            return new RangeDivisionInfo(timeRange, Years.yearsIn(timeRange).get(timeUnit.getDurationType()) + 1, TimeUnits.YEARS, ISODateTimeFormat.year(), lower, upper);
+        } else if (Months.monthsIn(timeRange).isGreaterThan(Months.THREE)) {
+            timeUnit = DateTimeFieldType.monthOfYear();
+            long lower = startWithZone.property(timeUnit).roundFloorCopy().getMillis();
+            long upper = endWithZone.property(timeUnit).roundCeilingCopy().getMillis();
+            return new RangeDivisionInfo(timeRange, Months.monthsIn(timeRange).getMonths() + 1, TimeUnits.MONTHS, DateTimeFormat.forPattern("YYYY'-'MMMM"), lower, upper); // NON-NLS
+        } else if (Days.daysIn(timeRange).isGreaterThan(Days.THREE)) {
+            timeUnit = DateTimeFieldType.dayOfMonth();
+            long lower = startWithZone.property(timeUnit).roundFloorCopy().getMillis();
+            long upper = endWithZone.property(timeUnit).roundCeilingCopy().getMillis();
+            return new RangeDivisionInfo(timeRange, Days.daysIn(timeRange).getDays() + 1, TimeUnits.DAYS, DateTimeFormat.forPattern("YYYY'-'MMMM'-'dd"), lower, upper); // NON-NLS
+        } else if (Hours.hoursIn(timeRange).isGreaterThan(Hours.THREE)) {
+            timeUnit = DateTimeFieldType.hourOfDay();
+            long lower = startWithZone.property(timeUnit).roundFloorCopy().getMillis();
+            long upper = endWithZone.property(timeUnit).roundCeilingCopy().getMillis();
+            return new RangeDivisionInfo(timeRange, Hours.hoursIn(timeRange).getHours() + 1, TimeUnits.HOURS, DateTimeFormat.forPattern("YYYY'-'MMMM'-'dd HH"), lower, upper); // NON-NLS
+        } else if (Minutes.minutesIn(timeRange).isGreaterThan(Minutes.THREE)) {
+            timeUnit = DateTimeFieldType.minuteOfHour();
+            long lower = startWithZone.property(timeUnit).roundFloorCopy().getMillis();
+            long upper = endWithZone.property(timeUnit).roundCeilingCopy().getMillis();
+            return new RangeDivisionInfo(timeRange, Minutes.minutesIn(timeRange).getMinutes() + 1, TimeUnits.MINUTES, DateTimeFormat.forPattern("YYYY'-'MMMM'-'dd HH':'mm"), lower, upper); // NON-NLS
+        } else {
+            timeUnit = DateTimeFieldType.secondOfMinute();
+            long lower = startWithZone.property(timeUnit).roundFloorCopy().getMillis();
+            long upper = endWithZone.property(timeUnit).roundCeilingCopy().getMillis();
+            return new RangeDivisionInfo(timeRange, Seconds.secondsIn(timeRange).getSeconds() + 1, TimeUnits.SECONDS, DateTimeFormat.forPattern("YYYY'-'MMMM'-'dd HH':'mm':'ss"), lower, upper); // NON-NLS
+        }
+    }
+
+    public DateTimeFormatter getTickFormatter() {
+        return tickFormatter;
+    }
+
+    public int getPeriodsInRange() {
+        return numberOfBlocks;
+    }
+
+    public TimeUnits getPeriodSize() {
+        return blockSize;
+    }
+
+    public long getUpperBound() {
+        return upperBound;
+    }
+
+    public long getLowerBound() {
+        return lowerBound;
+    }
+
+    @SuppressWarnings("ReturnOfCollectionOrArrayField")
+    synchronized public List<Interval> getIntervals(DateTimeZone tz) {
+        if (intervals == null) {
+            ArrayList<Interval> tempList = new ArrayList<>();
+            //extend range to block bounderies (ie day, month, year)
+            final Interval range = new Interval(new DateTime(lowerBound, tz), new DateTime(upperBound, tz));
+
+            DateTime start = range.getStart();
+            while (range.contains(start)) {
+                //increment for next iteration
+                DateTime end = start.plus(getPeriodSize().getPeriod());
+                final Interval interval = new Interval(start, end);
+                tempList.add(interval);
+                start = end;
+            }
+            intervals = ImmutableList.copyOf(tempList);
+        }
+        return intervals;
+    }
+
+    public String formatForTick(Interval interval) {
+        return interval.getStart().toString(tickFormatter);
+    }
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/RootEventType.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/RootEventType.java
new file mode 100644
index 0000000000000000000000000000000000000000..9f8921a9021b72b11cfbb0d4a1a8834aada28a94
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/RootEventType.java
@@ -0,0 +1,96 @@
+/*
+ * 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.datamodel.timeline;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import javafx.scene.image.Image;
+import javafx.scene.paint.Color;
+
+/**
+ * A singleton EventType to represent the root type of all event types.
+ */
+public class RootEventType implements EventType {
+
+	@Override
+	public List<RootEventType> getSiblingTypes() {
+		return Collections.singletonList(this);
+	}
+
+	@Override
+	public EventTypeZoomLevel getZoomLevel() {
+		return EventTypeZoomLevel.ROOT_TYPE;
+	}
+
+	private RootEventType() {
+	}
+
+	public static RootEventType getInstance() {
+		return RootEventTypeHolder.INSTANCE;
+	}
+
+	@Override
+	public EventType getSubType(String string) {
+		return BaseTypes.valueOf(string);
+	}
+
+	@Override
+	public int ordinal() {
+		return 0;
+	}
+
+	private static class RootEventTypeHolder {
+
+		private static final RootEventType INSTANCE = new RootEventType();
+
+		private RootEventTypeHolder() {
+		}
+	}
+
+	@Override
+	public Color getColor() {
+		return Color.hsb(359, .9, .9, 0);
+	}
+
+	@Override
+	public RootEventType getSuperType() {
+		return this;
+	}
+
+	@Override
+	public List<BaseTypes> getSubTypes() {
+		return Arrays.asList(BaseTypes.values());
+	}
+
+	@Override
+	public String getIconBase() {
+		throw new UnsupportedOperationException("Not supported yet."); // NON-NLS //To change body of generated methods, choose Tools | Templates.
+	}
+
+	@Override
+	public String getDisplayName() {
+		return BundleUtils.getBundle().getString("RootEventType.eventTypes.name");
+	}
+
+	@Override
+	public Image getFXImage() {
+		return null;
+	}
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/SingleEvent.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/SingleEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..2c85c0565c72b94eb6f68b640db7ef0b0c4f2bd8
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/SingleEvent.java
@@ -0,0 +1,320 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014-16 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.datamodel.timeline;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSortedSet;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Optional;
+import java.util.Set;
+import java.util.SortedSet;
+import org.joda.time.Interval;
+import org.sleuthkit.datamodel.TskData;
+
+/**
+ * A single event.
+ */
+public class SingleEvent implements TimeLineEvent {
+
+    private final long eventID;
+    /**
+     * The TSK object ID of the file this event is derived from.
+     */
+    private final long objID;
+
+    /**
+     * The TSK artifact ID of the file this event is derived from. Null, if this
+     * event is not derived from an artifact.
+     */
+    private final Long artifactID;
+
+    /**
+     * The TSK datasource ID of the datasource this event belongs to.
+     */
+    private final long dataSourceID;
+
+    /**
+     * The time of this event in second from the Unix epoch.
+     */
+    private final long time;
+    /**
+     * The type of this event.
+     */
+    private final EventType type;
+
+    /**
+     * The three descriptions (full, med, short) stored in a map, keyed by
+     * DescriptionLOD (Level of Detail)
+     */
+    private final ImmutableMap<DescriptionLoD, String> descriptions;
+
+    /**
+     * The known value for the file this event is derived from.
+     */
+    private final TskData.FileKnown known;
+
+    /**
+     * True if the file this event is derived from hits any of the configured
+     * hash sets.
+     */
+    private final boolean hashHit;
+
+    /**
+     * True if the file or artifact this event is derived from is tagged.
+     */
+    private final boolean tagged;
+
+    /**
+     * Single events may or may not have their parent set, since the parent is a
+     * transient property of the current (details) view settings.
+     */
+    private MultiEvent<?> parent = null;
+
+    public SingleEvent(long eventID, long dataSourceID, long objID,  Long artifactID, long time, EventType type, String fullDescription, String medDescription, String shortDescription, TskData.FileKnown known, boolean hashHit, boolean tagged) {
+        this.eventID = eventID;
+        this.dataSourceID = dataSourceID;
+        this.objID = objID;
+        this.artifactID = Long.valueOf(0).equals(artifactID) ? null : artifactID;
+        this.time = time;
+        this.type = type;
+        descriptions = ImmutableMap.<DescriptionLoD, String>of(DescriptionLoD.FULL, fullDescription,
+                DescriptionLoD.MEDIUM, medDescription,
+                DescriptionLoD.SHORT, shortDescription);
+        this.known = known;
+        this.hashHit = hashHit;
+        this.tagged = tagged;
+    }
+
+    /**
+     * Get a new SingleEvent that is the same as this event, but with the given
+     * parent.
+     *
+     * @param newParent the parent of the new event object.
+     *
+     * @return a new SingleEvent that is the same as this event, but with the
+     *         given parent.
+     */
+    public SingleEvent withParent(MultiEvent<?> newParent) {
+        SingleEvent singleEvent = new SingleEvent(eventID, dataSourceID, objID, artifactID, time, type, descriptions.get(DescriptionLoD.FULL), descriptions.get(DescriptionLoD.MEDIUM), descriptions.get(DescriptionLoD.SHORT), known, hashHit, tagged);
+        singleEvent.parent = newParent;
+        return singleEvent;
+    }
+
+    /**
+     * Is the file or artifact this event is derived from tagged?
+     *
+     * @return true if he file or artifact this event is derived from is tagged.
+     */
+    public boolean isTagged() {
+        return tagged;
+    }
+
+    /**
+     * Is the file this event is derived from in any of the configured hash
+     * sets.
+     *
+     *
+     * @return True if the file this event is derived from is in any of the
+     *         configured hash sets.
+     */
+    public boolean isHashHit() {
+        return hashHit;
+    }
+
+    /**
+     * Get the artifact id of the artifact this event is derived from.
+     *
+     * @return An Optional containing the artifact ID. Will be empty if this
+     *         event is not derived from an artifact
+     */
+    public Optional<Long> getArtifactID() {
+        return Optional.ofNullable(artifactID);
+    }
+
+    /**
+     * Get the event id of this event.
+     *
+     * @return The event id of this event.
+     */
+    public long getEventID() {
+        return eventID;
+    }
+
+    /**
+     * Get the obj id of the file this event is derived from.
+     *
+     * @return the object id.
+     */
+    public long getFileID() {
+        return objID;
+    }
+
+    /**
+     * Get the time of this event (in seconds from the Unix epoch).
+     *
+     * @return the time of this event in seconds from Unix epoch
+     */
+    public long getTime() {
+        return time;
+    }
+
+    @Override
+    public EventType getEventType() {
+        return type;
+    }
+
+    /**
+     * Get the full description of this event.
+     *
+     * @return the full description
+     */
+    public String getFullDescription() {
+        return getDescription(DescriptionLoD.FULL);
+    }
+
+    /**
+     * Get the medium description of this event.
+     *
+     * @return the medium description
+     */
+    public String getMedDescription() {
+        return getDescription(DescriptionLoD.MEDIUM);
+    }
+
+    /**
+     * Get the short description of this event.
+     *
+     * @return the short description
+     */
+    public String getShortDescription() {
+        return getDescription(DescriptionLoD.SHORT);
+    }
+
+    /**
+     * Get the known value of the file this event is derived from.
+     *
+     * @return the known value
+     */
+    public TskData.FileKnown getKnown() {
+        return known;
+    }
+
+    /**
+     * Get the description of this event at the give level of detail(LoD).
+     *
+     * @param lod The level of detail to get.
+     *
+     * @return The description of this event at the given level of detail.
+     */
+    public String getDescription(DescriptionLoD lod) {
+        return descriptions.get(lod);
+    }
+
+    /**
+     * Get the datasource id of the datasource this event belongs to.
+     *
+     * @return the datasource id.
+     */
+    public long getDataSourceID() {
+        return dataSourceID;
+    }
+
+    @Override
+    public Set<Long> getEventIDs() {
+        return Collections.singleton(eventID);
+    }
+
+    @Override
+    public Set<Long> getEventIDsWithHashHits() {
+        return isHashHit() ? Collections.singleton(eventID) : Collections.emptySet();
+    }
+
+    @Override
+    public Set<Long> getEventIDsWithTags() {
+        return isTagged() ? Collections.singleton(eventID) : Collections.emptySet();
+    }
+
+    @Override
+    public long getEndMillis() {
+        return time * 1000;
+    }
+
+    @Override
+    public long getStartMillis() {
+        return time * 1000;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        hash = 13 * hash + (int) (this.eventID ^ (this.eventID >>> 32));
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final SingleEvent other = (SingleEvent) obj;
+        if (this.eventID != other.eventID) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public SortedSet<EventCluster> getClusters() {
+        EventCluster eventCluster = new EventCluster(new Interval(time * 1000, time * 1000), type, getEventIDs(), getEventIDsWithHashHits(), getEventIDsWithTags(), getFullDescription(), DescriptionLoD.FULL);
+        return ImmutableSortedSet.orderedBy(Comparator.comparing(EventCluster::getStartMillis)).add(eventCluster).build();
+    }
+
+    @Override
+    public String getDescription() {
+        return getFullDescription();
+    }
+
+    @Override
+    public DescriptionLoD getDescriptionLoD() {
+        return DescriptionLoD.FULL;
+    }
+
+    /**
+     * get the EventStripe (if any) that contains this event, skipping over any
+     * intervening event cluster
+     *
+     * @return an Optional containing the parent stripe of this cluster: empty
+     *         if the cluster has no parent set or the parent has no parent
+     *         stripe.
+     */
+    @Override
+    public Optional<EventStripe> getParentStripe() {
+        if (parent == null) {
+            return Optional.empty();
+        } else if (parent instanceof EventStripe) {
+            return Optional.of((EventStripe) parent);
+        } else {
+            return parent.getParentStripe();
+        }
+    }
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/TimeLineEvent.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/TimeLineEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..404e692233e10a0505cc8d772956c7b1e7912a23
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/TimeLineEvent.java
@@ -0,0 +1,121 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2016 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.datamodel.timeline;
+
+import java.util.Optional;
+import java.util.Set;
+import java.util.SortedSet;
+import org.sleuthkit.datamodel.timeline.DescriptionLoD;
+
+/**
+ * An event of the timeline. Concrete implementations may represent single
+ * events or multiple events grouped together based on some common properties
+ * (for example close together in time and or having similar descriptions or
+ * event types). Note that for SingleEvents or events that are all simultaneous,
+ * the start time may be equal to the end time.
+ */
+public interface TimeLineEvent {
+
+    /**
+     * Get a description of this event. Implementations may choose what level of
+     * description to provide.
+     *
+     * @return A description of this event.
+     */
+    public String getDescription();
+
+    /**
+     * Get the Description level of detail at which all single events of this
+     * event have the same description, ie, what level of detail was used to
+     * group these events.
+     *
+     * @return the description level of detail of the given events
+     */
+    public DescriptionLoD getDescriptionLoD();
+
+    /**
+     * get the EventStripe (if any) that contains this event.
+     *
+     * @return an Optional containing the parent stripe of this event, or is
+     *         empty if the event has no parent stripe.
+     */
+    public Optional<EventStripe> getParentStripe();
+
+    /**
+     * Get the id(s) of this event as a set.
+     *
+     * @return a Set containing the event id(s) of this event.
+     */
+    Set<Long> getEventIDs();
+
+    /**
+     * Get the id(s) of this event that have hash hits associated with them.
+     *
+     * @return a Set containing the event id(s) of this event that have hash
+     *         hits associated with them.
+     */
+    Set<Long> getEventIDsWithHashHits();
+
+    /**
+     * Get the id(s) of this event that have tags associated with them.
+     *
+     * @return a Set containing the event id(s) of this event that have tags
+     *         associated with them.
+     */
+    Set<Long> getEventIDsWithTags();
+
+    /**
+     * Get the EventType of this event.
+     *
+     * @return the EventType of this event.
+     */
+    EventType getEventType();
+
+    /**
+     * Get the start time of this event as milliseconds from the Unix Epoch.
+     *
+     * @return the start time of this event as milliseconds from the Unix Epoch.
+     */
+    long getEndMillis();
+
+    /**
+     * Get the end time of this event as milliseconds from the Unix Epoch.
+     *
+     * @return the end time of this event as milliseconds from the Unix Epoch.
+     */
+    long getStartMillis();
+
+    /**
+     * Get the number of SingleEvents this event contains.
+     *
+     * @return the number of SingleEvents this event contains.
+     */
+    default int getSize() {
+        return getEventIDs().size();
+    }
+
+    /**
+     * Get the EventClusters that make up this event. May be null for
+     * SingleEvents, or return a refernece to this event if it is an
+     * EventCluster
+     *
+     * @return The EventClusters that make up this event.
+     */
+    SortedSet<EventCluster> getClusters();
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/TimeUnits.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/TimeUnits.java
new file mode 100644
index 0000000000000000000000000000000000000000..da70fcaf3eecf0b292ff663a92c39c1ccc5df8e8
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/TimeUnits.java
@@ -0,0 +1,96 @@
+/*
+ * 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.datamodel.timeline;
+
+import org.sleuthkit.datamodel.timeline.DisplayNameProvider;
+import java.time.temporal.ChronoUnit;
+import org.joda.time.Days;
+import org.joda.time.Hours;
+import org.joda.time.Minutes;
+import org.joda.time.Months;
+import org.joda.time.Period;
+import org.joda.time.Seconds;
+import org.joda.time.Years;
+
+/**
+ * predefined units of time for use in choosing axis labels and sub intervals.
+ */
+public enum TimeUnits implements DisplayNameProvider {
+
+    FOREVER(null, ChronoUnit.FOREVER),
+    YEARS(Years.ONE.toPeriod(), ChronoUnit.YEARS),
+    MONTHS(Months.ONE.toPeriod(), ChronoUnit.MONTHS),
+    DAYS(Days.ONE.toPeriod(), ChronoUnit.DAYS),
+    HOURS(Hours.ONE.toPeriod(), ChronoUnit.HOURS),
+    MINUTES(Minutes.ONE.toPeriod(), ChronoUnit.MINUTES),
+    SECONDS(Seconds.ONE.toPeriod(), ChronoUnit.SECONDS);
+
+    public static TimeUnits fromChronoUnit(ChronoUnit chronoUnit) {
+        switch (chronoUnit) {
+
+            case FOREVER:
+                return FOREVER;
+            case ERAS:
+            case MILLENNIA:
+            case CENTURIES:
+            case DECADES:
+            case YEARS:
+                return YEARS;
+            case MONTHS:
+                return MONTHS;
+            case WEEKS:
+            case DAYS:
+                return DAYS;
+            case HOURS:
+            case HALF_DAYS:
+                return HOURS;
+            case MINUTES:
+                return MINUTES;
+            case SECONDS:
+            case MILLIS:
+            case MICROS:
+            case NANOS:
+                return SECONDS;
+            default:
+                return YEARS;
+        }
+    }
+
+    private final Period p;
+
+    private final ChronoUnit cu;
+
+    public Period getPeriod() {
+        return p;
+    }
+
+    public ChronoUnit getChronoUnit() {
+        return cu;
+    }
+
+    private TimeUnits(Period p, ChronoUnit cu) {
+        this.p = p;
+        this.cu = cu;
+    }
+
+    @Override
+    public String getDisplayName() {
+        return toString();
+    }
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/WebTypes.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/WebTypes.java
new file mode 100644
index 0000000000000000000000000000000000000000..963781dedd9a14f489ea0695703032d3a56fe244
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/WebTypes.java
@@ -0,0 +1,210 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014-16 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.datamodel.timeline;
+
+import com.google.common.net.InternetDomainName;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Function;
+import javafx.scene.image.Image;
+import org.apache.commons.lang3.StringUtils;
+import org.sleuthkit.datamodel.BlackboardArtifact;
+import org.sleuthkit.datamodel.BlackboardAttribute;
+import org.sleuthkit.datamodel.TskCoreException;
+import org.sleuthkit.datamodel.timeline.ArtifactEventType.AttributeEventDescription;
+import org.sleuthkit.datamodel.timeline.ArtifactEventType.AttributeExtractor;
+
+/**
+ *
+ */
+public enum WebTypes implements EventType, ArtifactEventType {
+
+    WEB_DOWNLOADS(BundleUtils.getBundle().getString( "WebTypes.webDownloads.name"),
+            "downloads.png", // NON-NLS
+            new BlackboardArtifact.Type(BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_DOWNLOAD),
+            new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_ACCESSED),
+            TopPrivateDomainExtractor.getInstance(),
+            new AttributeExtractor(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH)),
+            new AttributeExtractor(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL))) {
+
+                @Override
+        public AttributeEventDescription parseAttributesHelper(BlackboardArtifact artf) throws TskCoreException {
+            long time = artf.getAttribute(getDateTimeAttributeType()).getValueLong();
+                    String domain = getShortExtractor().apply(artf);
+                    String path = getMedExtractor().apply(artf);
+                    String fileName = StringUtils.substringAfterLast(path, "/");
+                    String url = getFullExtractor().apply(artf);
+
+                    //TODO: review non default description construction
+                    String shortDescription = fileName + " from " + domain; // NON-NLS
+                    String medDescription = fileName + " from " + url; // NON-NLS
+                    String fullDescription = path + " from " + url; // NON-NLS
+                    return new AttributeEventDescription(time, shortDescription, medDescription, fullDescription);
+                }
+            },
+    //TODO: review description separators
+    WEB_COOKIE(BundleUtils.getBundle().getString( "WebTypes.webCookies.name"),
+            "cookies.png", // NON-NLS
+            new BlackboardArtifact.Type(BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_COOKIE),
+            new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME),
+            TopPrivateDomainExtractor.getInstance(),
+            new AttributeExtractor(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME)),
+            new AttributeExtractor(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_VALUE))),
+    //TODO: review description separators
+    WEB_BOOKMARK(BundleUtils.getBundle().getString( "WebTypes.webBookmarks.name"),
+            "bookmarks.png", // NON-NLS
+            new BlackboardArtifact.Type(BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_BOOKMARK),
+            new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED),
+            TopPrivateDomainExtractor.getInstance(),
+            new AttributeExtractor(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL)),
+            new AttributeExtractor(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TITLE))),
+    //TODO: review description separators
+    WEB_HISTORY(BundleUtils.getBundle().getString( "WebTypes.webHistory.name"),
+            "history.png", // NON-NLS
+            new BlackboardArtifact.Type(BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_HISTORY),
+            new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_ACCESSED),
+            TopPrivateDomainExtractor.getInstance(),
+            new AttributeExtractor(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL)),
+            new AttributeExtractor(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TITLE))),
+    //TODO: review description separators
+    WEB_SEARCH(BundleUtils.getBundle().getString( "WebTypes.webSearch.name"),
+            "searchquery.png", // NON-NLS
+            new BlackboardArtifact.Type(BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_SEARCH_QUERY),
+            new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_ACCESSED),
+            new AttributeExtractor(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TEXT)),
+            TopPrivateDomainExtractor.getInstance(),
+            new AttributeExtractor(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME)));
+
+    private final BlackboardAttribute.Type dateTimeAttributeType;
+
+    private final String iconBase;
+
+    private final Image image;
+
+    @Override
+    public Image getFXImage() {
+        return image;
+    }
+
+    @Override
+    public BlackboardAttribute.Type getDateTimeAttributeType() {
+        return dateTimeAttributeType;
+    }
+
+    @Override
+    public EventTypeZoomLevel getZoomLevel() {
+        return EventTypeZoomLevel.SUB_TYPE;
+    }
+
+    private final Function<BlackboardArtifact, String> longExtractor;
+
+    private final Function<BlackboardArtifact, String> medExtractor;
+
+    private final Function<BlackboardArtifact, String> shortExtractor;
+
+    @Override
+    public Function<BlackboardArtifact, String> getFullExtractor() {
+        return longExtractor;
+    }
+
+    @Override
+    public Function<BlackboardArtifact, String> getMedExtractor() {
+        return medExtractor;
+    }
+
+    @Override
+    public Function<BlackboardArtifact, String> getShortExtractor() {
+        return shortExtractor;
+    }
+
+    private final String displayName;
+
+    private final BlackboardArtifact.Type artifactType;
+
+    @Override
+    public String getIconBase() {
+        return iconBase;
+    }
+
+    @Override
+    public BlackboardArtifact.Type getArtifactType() {
+        return artifactType;
+    }
+
+    private WebTypes(String displayName, String iconBase, BlackboardArtifact.Type artifactType,
+            BlackboardAttribute.Type dateTimeAttributeType,
+            Function<BlackboardArtifact, String> shortExtractor,
+            Function<BlackboardArtifact, String> medExtractor,
+            Function<BlackboardArtifact, String> longExtractor) {
+        this.displayName = displayName;
+        this.iconBase = iconBase;
+        this.artifactType = artifactType;
+        this.dateTimeAttributeType = dateTimeAttributeType;
+        this.shortExtractor = shortExtractor;
+        this.medExtractor = medExtractor;
+        this.longExtractor = longExtractor;
+        this.image = new Image("org/sleuthkit/autopsy/timeline/images/" + iconBase, true); // NON-NLS
+    }
+
+    @Override
+    public EventType getSuperType() {
+        return BaseTypes.WEB_ACTIVITY;
+    }
+
+    @Override
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    @Override
+    public EventType getSubType(String string) {
+        return WebTypes.valueOf(string);
+    }
+
+    @Override
+    public List<? extends EventType> getSubTypes() {
+        return Collections.emptyList();
+    }
+
+    private static class TopPrivateDomainExtractor extends AttributeExtractor {
+
+        final private static TopPrivateDomainExtractor instance = new TopPrivateDomainExtractor();
+
+        static TopPrivateDomainExtractor getInstance() {
+            return instance;
+        }
+
+        @Override
+        public String apply(BlackboardArtifact artf) {
+            String domainString = StringUtils.substringBefore(super.apply(artf), "/");
+            if (InternetDomainName.isValid(domainString)) {
+                InternetDomainName domain = InternetDomainName.from(domainString);
+                return (domain.isUnderPublicSuffix())
+                        ? domain.topPrivateDomain().toString()
+                        : domain.toString();
+            } else {
+                return domainString;
+            }
+        }
+
+        TopPrivateDomainExtractor() {
+            super(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN));
+        }
+    }
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/ZoomParams.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/ZoomParams.java
new file mode 100644
index 0000000000000000000000000000000000000000..1ccbfc3811d998dfe689baa67c603d3a8ef030fc
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/ZoomParams.java
@@ -0,0 +1,136 @@
+/*
+ * 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.datamodel.timeline;
+
+import java.util.Objects;
+import org.joda.time.Interval;
+import org.sleuthkit.datamodel.timeline.filters.RootFilter;
+
+/**
+ * This class encapsulates all the zoom(and filter) parameters into one object
+ * for passing around and as a memento of the zoom/filter state.
+ */
+public class ZoomParams {
+
+    private final Interval timeRange;
+
+    private final EventTypeZoomLevel typeZoomLevel;
+
+    private final RootFilter filter;
+
+    private final DescriptionLoD descrLOD;
+
+    public Interval getTimeRange() {
+        return timeRange;
+    }
+
+    public EventTypeZoomLevel getTypeZoomLevel() {
+        return typeZoomLevel;
+    }
+
+    public RootFilter getFilter() {
+        return filter;
+    }
+
+    public DescriptionLoD getDescriptionLOD() {
+        return descrLOD;
+    }
+
+    public ZoomParams(Interval timeRange, EventTypeZoomLevel zoomLevel, RootFilter filter, DescriptionLoD descrLOD) {
+        this.timeRange = timeRange;
+        this.typeZoomLevel = zoomLevel;
+        this.filter = filter;
+        this.descrLOD = descrLOD;
+    }
+
+    public ZoomParams withTimeAndType(Interval timeRange, EventTypeZoomLevel zoomLevel) {
+        return new ZoomParams(timeRange, zoomLevel, filter, descrLOD);
+    }
+
+    public ZoomParams withTypeZoomLevel(EventTypeZoomLevel zoomLevel) {
+        return new ZoomParams(timeRange, zoomLevel, filter, descrLOD);
+    }
+
+    public ZoomParams withTimeRange(Interval timeRange) {
+        return new ZoomParams(timeRange, typeZoomLevel, filter, descrLOD);
+    }
+
+    public ZoomParams withDescrLOD(DescriptionLoD descrLOD) {
+        return new ZoomParams(timeRange, typeZoomLevel, filter, descrLOD);
+    }
+
+    public ZoomParams withFilter(RootFilter filter) {
+        return new ZoomParams(timeRange, typeZoomLevel, filter, descrLOD);
+    }
+
+    public boolean hasFilter(RootFilter filterSet) {
+        return this.filter.equals(filterSet);
+    }
+
+    public boolean hasTypeZoomLevel(EventTypeZoomLevel typeZoom) {
+        return this.typeZoomLevel.equals(typeZoom);
+    }
+
+    public boolean hasTimeRange(Interval timeRange) {
+        return this.timeRange == null ? false : this.timeRange.equals(timeRange);
+    }
+
+    public boolean hasDescrLOD(DescriptionLoD newLOD) {
+        return this.descrLOD.equals(newLOD);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 3;
+        hash = 97 * hash + Objects.hashCode(this.timeRange.getStartMillis());
+        hash = 97 * hash + Objects.hashCode(this.timeRange.getEndMillis());
+        hash = 97 * hash + Objects.hashCode(this.typeZoomLevel);
+        hash = 97 * hash + Objects.hashCode(this.filter.isSelected());
+        hash = 97 * hash + Objects.hashCode(this.descrLOD);
+
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final ZoomParams other = (ZoomParams) obj;
+        if (!Objects.equals(this.timeRange, other.timeRange)) {
+            return false;
+        }
+        if (this.typeZoomLevel != other.typeZoomLevel) {
+            return false;
+        }
+        if (this.filter.equals(other.filter) == false) {
+            return false;
+        }
+        return this.descrLOD == other.descrLOD;
+    }
+
+    @Override
+    public String toString() {
+        return "ZoomParams{" + "timeRange=" + timeRange + ", typeZoomLevel=" + typeZoomLevel + ", filter=" + filter + ", descrLOD=" + descrLOD + '}'; //NON-NLS
+    }
+
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/AbstractFilter.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/AbstractFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..ee5506b3a1a6c956674b1198c93596a1fbe0250e
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/AbstractFilter.java
@@ -0,0 +1,77 @@
+/*
+ * 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.datamodel.timeline.filters;
+
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.BooleanBinding;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.value.ObservableBooleanValue;
+
+/**
+ * Base implementation of a {@link Filter}. Implements active property.
+ *
+ */
+public abstract class AbstractFilter implements Filter {
+
+    private final SimpleBooleanProperty selected = new SimpleBooleanProperty(true);
+    private final SimpleBooleanProperty disabled = new SimpleBooleanProperty(false);
+    private final BooleanBinding activeProperty = Bindings.and(selected, disabled.not());
+
+    @Override
+    public SimpleBooleanProperty selectedProperty() {
+        return selected;
+    }
+
+    @Override
+    public ObservableBooleanValue disabledProperty() {
+        return disabled;
+    }
+
+    @Override
+    public void setSelected(Boolean act) {
+        selected.set(act);
+    }
+
+    @Override
+    public boolean isSelected() {
+        return selected.get();
+    }
+
+    @Override
+    public void setDisabled(Boolean act) {
+        disabled.set(act);
+    }
+
+    @Override
+    public boolean isDisabled() {
+        return disabledProperty().get();
+    }
+
+    
+
+    @Override
+    public boolean isActive() {
+        return activeProperty().get();
+    }
+
+    @Override
+    public BooleanBinding activeProperty() {
+        return activeProperty;
+    }
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/Bundle.properties b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/Bundle.properties
new file mode 100644
index 0000000000000000000000000000000000000000..d62fb3444484e90aaac13d542a7193b197511727
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/Bundle.properties
@@ -0,0 +1,10 @@
+DataSourcesFilter.displayName.text=Data Source
+DescriptionFilter.mode.exclude=Exclude
+DescriptionFilter.mode.include=Include
+hashHitsFilter.displayName.text=Hash Sets
+hideKnownFilter.displayName.text=Hide Known Files
+# {0} - sub filter displaynames
+IntersectionFilter.displayName.text=Intersection{0}
+tagsFilter.displayName.text=Tags
+TextFilter.displayName.text=Text Filter
+TypeFilter.displayName.text=Event Type
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/BundleUtils.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/BundleUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..4636e5a1747b1f40ecb7dfa1da21277c52a0b321
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/BundleUtils.java
@@ -0,0 +1,15 @@
+package org.sleuthkit.datamodel.timeline.filters;
+
+
+import org.sleuthkit.datamodel.timeline.*;
+import java.util.ResourceBundle;
+
+
+class BundleUtils {
+
+	private static final ResourceBundle BUNDLE = ResourceBundle.getBundle("org.sleuthkit.datamodel.timeline.filters.Bundle");
+
+	static ResourceBundle getBundle() {
+		return BUNDLE;
+	}
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/Bundle_ja.properties b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/Bundle_ja.properties
new file mode 100644
index 0000000000000000000000000000000000000000..00dca873f4e135175a7412f80e9b7fd25a445ee0
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/Bundle_ja.properties
@@ -0,0 +1,9 @@
+hideKnownFilter.displayName.text=\u65E2\u77E5\u30D5\u30A1\u30A4\u30EB\u3092\u96A0\u3059
+TextFilter.displayName.text=\u30C6\u30AD\u30B9\u30C8\u30D5\u30A3\u30EB\u30BF\u30FC
+TypeFilter.displayName.text=\u30A4\u30D9\u30F3\u30C8\u30BF\u30A4\u30D7\u30D5\u30A3\u30EB\u30BF\u30FC
+IntersectionFilter.displayName.text=\u30A4\u30F3\u30BF\u30FC\u30BB\u30AF\u30B7\u30E7\u30F3{0}
+DataSourcesFilter.displayName.text=\u30C7\u30FC\u30BF\u30BD\u30FC\u30B9
+DescriptionFilter.mode.exclude=\u9664\u5916\u3059\u308B
+DescriptionFilter.mode.include=\u542B\u3080
+hashHitsFilter.displayName.text=\u30CF\u30C3\u30B7\u30E5\u30BB\u30C3\u30C8\u306E\u30D2\u30C3\u30C8\u306E\u307F
+tagsFilter.displayName.text=\u30BF\u30B0\u3055\u308C\u305F\u30A4\u30D9\u30F3\u30C8\u306E\u307F
\ No newline at end of file
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/CompoundFilter.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/CompoundFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..5ce00cd6c8456dde16a838712728a6f651575e15
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/CompoundFilter.java
@@ -0,0 +1,92 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014-16 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.datamodel.timeline.filters;
+
+import java.util.List;
+import java.util.Objects;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+
+/**
+ * A Filter with a collection of {@link Filter} sub-filters. If this filter is
+ * not active than none of its sub-filters are applied either. Concrete
+ * implementations can decide how to combine the sub-filters.
+ *
+ * a {@link CompoundFilter} uses listeners to enforce the following
+ * relationships between it and its sub-filters: if all of a compound filter's
+ * sub-filters become un-selected, un-select the compound filter.
+ */
+public abstract class CompoundFilter<SubFilterType extends Filter> extends AbstractFilter {
+
+    /**
+     * the list of sub-filters that make up this filter
+     */
+    private final ObservableList<SubFilterType> subFilters = FXCollections.observableArrayList();
+
+    public final ObservableList<SubFilterType> getSubFilters() {
+        return subFilters;
+    }
+
+    /**
+     * construct a compound filter from a list of other filters to combine.
+     *
+     * @param subFilters
+     */
+    public CompoundFilter(List<SubFilterType> subFilters) {
+        super();
+
+        //listen to changes in list of subfilters 
+        this.subFilters.addListener((ListChangeListener.Change<? extends SubFilterType> change) -> {
+            while (change.next()) {
+                //add a listener to the selected property of each added subfilter
+                change.getAddedSubList().forEach(addedSubFilter -> {
+                    //if a subfilter's selected property changes...
+                    addedSubFilter.selectedProperty().addListener(selectedProperty -> {
+                        //set this compound filter selected af any of the subfilters are selected.
+                        setSelected(getSubFilters().parallelStream().anyMatch(Filter::isSelected));
+                    });
+                });
+            }
+        });
+
+        this.subFilters.setAll(subFilters);
+    }
+
+    static <SubFilterType extends Filter> boolean areSubFiltersEqual(final CompoundFilter<SubFilterType> oneFilter, final CompoundFilter<SubFilterType> otherFilter) {
+        if (oneFilter.getSubFilters().size() != otherFilter.getSubFilters().size()) {
+            return false;
+        }
+        for (int i = 0; i < oneFilter.getSubFilters().size(); i++) {
+            final SubFilterType subFilter = oneFilter.getSubFilters().get(i);
+            final SubFilterType otherSubFilter = otherFilter.getSubFilters().get(i);
+            if (subFilter.equals(otherSubFilter) == false) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 3;
+        hash = 61 * hash + Objects.hashCode(this.subFilters);
+        return hash;
+    }
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/DataSourceFilter.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/DataSourceFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..74411ec82c9169205b83164de3c2bb3cb62af858
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/DataSourceFilter.java
@@ -0,0 +1,82 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2015 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.datamodel.timeline.filters;
+
+import java.util.Objects;
+
+/**
+ * Filter for an individual datasource
+ */
+public class DataSourceFilter extends AbstractFilter {
+
+    private final String dataSourceName;
+    private final long dataSourceID;
+
+    public long getDataSourceID() {
+        return dataSourceID;
+    }
+
+    public String getDataSourceName() {
+        return dataSourceName;
+    }
+
+    public DataSourceFilter(String dataSourceName, long dataSourceID) {
+        this.dataSourceName = dataSourceName;
+        this.dataSourceID = dataSourceID;
+    }
+
+    @Override
+    synchronized public DataSourceFilter copyOf() {
+        DataSourceFilter filterCopy = new DataSourceFilter(getDataSourceName(), getDataSourceID());
+        filterCopy.setSelected(isSelected());
+        filterCopy.setDisabled(isDisabled());
+        return filterCopy;
+    }
+
+    @Override
+    public String getDisplayName() {
+        return getDataSourceName();
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 5;
+        hash = 97 * hash + Objects.hashCode(this.dataSourceName);
+        hash = 97 * hash + (int) (this.dataSourceID ^ (this.dataSourceID >>> 32));
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final DataSourceFilter other = (DataSourceFilter) obj;
+        if (!Objects.equals(this.dataSourceName, other.dataSourceName)) {
+            return false;
+        }
+        if (this.dataSourceID != other.dataSourceID) {
+            return false;
+        }
+        return isSelected() == other.isSelected();
+    }
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/DataSourcesFilter.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/DataSourcesFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..f87b229cf44f5c4d37768e18865a9b53edc4d967
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/DataSourcesFilter.java
@@ -0,0 +1,94 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2015-16 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.datamodel.timeline.filters;
+
+import java.util.function.Predicate;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.BooleanBinding;
+import javafx.beans.value.ObservableBooleanValue;
+
+/**
+ * union of {@link DataSourceFilter}s
+ */
+public class DataSourcesFilter extends UnionFilter<DataSourceFilter> {
+
+    //keep references to the overridden properties so they don't get GC'd
+    private final BooleanBinding activePropertyOverride;
+    private final BooleanBinding disabledPropertyOverride;
+
+    public DataSourcesFilter() {
+        disabledPropertyOverride = Bindings.or(super.disabledProperty(), Bindings.size(getSubFilters()).lessThanOrEqualTo(1));
+        activePropertyOverride = super.activeProperty().and(Bindings.not(disabledPropertyOverride));
+    }
+
+    @Override
+    public DataSourcesFilter copyOf() {
+        final DataSourcesFilter filterCopy = new DataSourcesFilter();
+        //add a copy of each subfilter
+        getSubFilters().forEach(dataSourceFilter -> filterCopy.addSubFilter(dataSourceFilter.copyOf()));
+        //these need to happen after the listeners fired by adding the subfilters 
+        filterCopy.setSelected(isSelected());
+        filterCopy.setDisabled(isDisabled());
+
+        return filterCopy;
+    }
+
+    @Override
+    public String getDisplayName() {
+		return BundleUtils.getBundle().getString("DataSourcesFilter.displayName.text");
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final DataSourcesFilter other = (DataSourcesFilter) obj;
+
+        if (isActive() != other.isActive()) {
+            return false;
+        }
+
+        return areSubFiltersEqual(this, other);
+
+    }
+
+    @Override
+    public int hashCode() {
+        return 9;
+    }
+
+    @Override
+    public ObservableBooleanValue disabledProperty() {
+        return disabledPropertyOverride;
+    }
+
+    @Override
+    public BooleanBinding activeProperty() {
+        return activePropertyOverride;
+    }
+
+    @Override
+    Predicate<DataSourceFilter> getDuplicatePredicate(DataSourceFilter subfilter) {
+        return dataSourcefilter -> dataSourcefilter.getDataSourceID() == subfilter.getDataSourceID();
+    }
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/DescriptionFilter.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/DescriptionFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..1d4213fdc6a3755afa36e00dee66c215fc504584
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/DescriptionFilter.java
@@ -0,0 +1,112 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2015 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.datamodel.timeline.filters;
+
+import java.util.Objects;
+import org.sleuthkit.datamodel.timeline.DescriptionLoD;
+
+public class DescriptionFilter extends AbstractFilter {
+
+	private final DescriptionLoD descriptionLoD;
+	private final String description;
+	private final FilterMode filterMode;
+
+	public FilterMode getFilterMode() {
+		return filterMode;
+	}
+
+	public DescriptionFilter(DescriptionLoD descriptionLoD, String description, FilterMode filterMode) {
+		this.descriptionLoD = descriptionLoD;
+		this.description = description;
+		this.filterMode = filterMode;
+	}
+
+	@Override
+	public DescriptionFilter copyOf() {
+		DescriptionFilter filterCopy = new DescriptionFilter(getDescriptionLoD(), getDescription(), getFilterMode());
+		filterCopy.setSelected(isSelected());
+		filterCopy.setDisabled(isDisabled());
+		return filterCopy;
+	}
+
+	@Override
+	public String getDisplayName() {
+		return getDescriptionLoD().getDisplayName() + ": " + getDescription();
+	}
+
+	/**
+	 * @return the descriptionLoD
+	 */
+	public DescriptionLoD getDescriptionLoD() {
+		return descriptionLoD;
+	}
+
+	/**
+	 * @return the description
+	 */
+	public String getDescription() {
+		return description;
+	}
+
+	public enum FilterMode {
+
+		EXCLUDE(BundleUtils.getBundle().getString("DescriptionFilter.mode.exclude")),
+		INCLUDE(BundleUtils.getBundle().getString("DescriptionFilter.mode.include"));
+
+		private final String displayName;
+
+		private FilterMode(String displayName) {
+			this.displayName = displayName;
+		}
+
+		private String getDisplayName() {
+			return displayName;
+		}
+	}
+
+	@Override
+	public int hashCode() {
+		int hash = 7;
+		hash = 79 * hash + Objects.hashCode(this.descriptionLoD);
+		hash = 79 * hash + Objects.hashCode(this.description);
+		hash = 79 * hash + Objects.hashCode(this.filterMode);
+		return hash;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (obj == null) {
+			return false;
+		}
+		if (getClass() != obj.getClass()) {
+			return false;
+		}
+		final DescriptionFilter other = (DescriptionFilter) obj;
+		if (this.descriptionLoD != other.descriptionLoD) {
+			return false;
+		}
+		if (!Objects.equals(this.description, other.description)) {
+			return false;
+		}
+		if (this.filterMode != other.filterMode) {
+			return false;
+		}
+		return true;
+	}
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/Filter.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/Filter.java
new file mode 100644
index 0000000000000000000000000000000000000000..cda02768654756a480731ecc201954047ceb3117
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/Filter.java
@@ -0,0 +1,129 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014-15 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.datamodel.timeline.filters;
+
+import javafx.beans.binding.BooleanBinding;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.value.ObservableBooleanValue;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+/**
+ * Interface for Filters. Filters are given to the EventDB who interpretes them
+ * a appropriately for all db queries. Since the filters are primarily
+ * configured in the UI, this interface provides selected, disabled and active
+ * (selected and not disabled) properties.
+ */
+public interface Filter {
+
+    /**
+     * get a filter that is the intersection of the given filters
+     *
+     * @param filters a set of filters to intersect
+     *
+     * @return a filter that is the intersection of the given filters
+     */
+    public static IntersectionFilter<Filter> intersect(ObservableList<Filter> filters) {
+        return new IntersectionFilter<>(filters);
+    }
+
+    /**
+     * get a filter that is the intersection of the given filters
+     *
+     * @param filters a set of filters to intersect
+     *
+     * @return a filter that is the intersection of the given filters
+     */
+    public static IntersectionFilter<Filter> intersect(Filter[] filters) {
+        return intersect(FXCollections.observableArrayList(filters));
+    }
+
+    /**
+     * since filters have mutable state (selected/disabled/active) and are
+     * observed in various places, we need a mechanism to copy the current state
+     * to keep in the history.
+     *
+     * Concrete sub classes should implement this in a way that preserves the
+     * state and any sub-filters.
+     *
+     * @return a copy of this filter.
+     */
+    Filter copyOf();
+
+    /**
+     * get the display name of this filter
+     *
+     * @return a name for this filter to show in the UI
+     */
+    String getDisplayName();
+
+    /**
+     * is this filter selected
+     *
+     * @return true if this filter is selected
+     */
+    boolean isSelected();
+
+    /**
+     * set this filter selected
+     *
+     * @param selected true to selecte, false to un-select
+     */
+    void setSelected(Boolean selected);
+
+    /**
+     * observable selected property
+     *
+     * @return the observable selected property for this filter
+     */
+    SimpleBooleanProperty selectedProperty();
+
+    /**
+     * set the filter disabled
+     */
+    void setDisabled(Boolean act);
+
+    /**
+     * observable disabled property
+     *
+     * @return the observable disabled property for this filter
+     */
+    ObservableBooleanValue disabledProperty();
+
+    /**
+     * is this filter disabled
+     *
+     * @return true if this filter is disabled
+     */
+    boolean isDisabled();
+
+    /**
+     * is this filter active (selected and not disabled)
+     *
+     * @return true if this filter is active
+     */
+    boolean isActive();
+
+    /**
+     * observable active property
+     *
+     * @return the observable active property for this filter
+     */
+    BooleanBinding activeProperty();
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/HashHitsFilter.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/HashHitsFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..80caf081ef73fbd796bc7e9c964a717fcb2249a7
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/HashHitsFilter.java
@@ -0,0 +1,82 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2015-16 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.datamodel.timeline.filters;
+
+import java.util.function.Predicate;
+import javafx.beans.binding.Bindings;
+import javafx.beans.value.ObservableBooleanValue;
+
+/**
+ *
+ */
+public class HashHitsFilter extends UnionFilter<HashSetFilter> {
+
+	@Override
+	public String getDisplayName() {
+		return BundleUtils.getBundle().getString("hashHitsFilter.displayName.text");
+	}
+
+	public HashHitsFilter() {
+		setSelected(false);
+	}
+
+	@Override
+	public HashHitsFilter copyOf() {
+		HashHitsFilter filterCopy = new HashHitsFilter();
+		//add a copy of each subfilter
+		this.getSubFilters().forEach(hashSetFilter -> filterCopy.addSubFilter(hashSetFilter.copyOf()));
+		//these need to happen after the listeners fired by adding the subfilters 
+		filterCopy.setSelected(isSelected());
+		filterCopy.setDisabled(isDisabled());
+
+		return filterCopy;
+	}
+
+	@Override
+	public int hashCode() {
+		return 7;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (obj == null) {
+			return false;
+		}
+		if (getClass() != obj.getClass()) {
+			return false;
+		}
+		final HashHitsFilter other = (HashHitsFilter) obj;
+
+		if (isActive() != other.isActive()) {
+			return false;
+		}
+
+		return areSubFiltersEqual(this, other);
+	}
+
+	@Override
+	public ObservableBooleanValue disabledProperty() {
+		return Bindings.or(super.disabledProperty(), Bindings.isEmpty(getSubFilters()));
+	}
+
+	@Override
+	Predicate<HashSetFilter> getDuplicatePredicate(HashSetFilter subfilter) {
+		return hashSetFilter -> subfilter.getHashSetID() == hashSetFilter.getHashSetID();
+	}
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/HashSetFilter.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/HashSetFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..799da02a70cc1b7432a41ed9d96066f42dacc55e
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/HashSetFilter.java
@@ -0,0 +1,82 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2015 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.datamodel.timeline.filters;
+
+import java.util.Objects;
+
+/**
+ * Filter for an individual hash set
+ */
+public class HashSetFilter extends AbstractFilter {
+
+    private final String hashSetName;
+    private final long hashSetID;
+
+    public long getHashSetID() {
+        return hashSetID;
+    }
+
+    public String getHashSetName() {
+        return hashSetName;
+    }
+
+    public HashSetFilter(String hashSetName, long hashSetID) {
+        this.hashSetName = hashSetName;
+        this.hashSetID = hashSetID;
+    }
+
+    @Override
+    synchronized public HashSetFilter copyOf() {
+        HashSetFilter filterCopy = new HashSetFilter(getHashSetName(), getHashSetID());
+        filterCopy.setSelected(isSelected());
+        filterCopy.setDisabled(isDisabled());
+        return filterCopy;
+    }
+
+    @Override
+    public String getDisplayName() {
+        return hashSetName;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        hash = 37 * hash + Objects.hashCode(this.hashSetName);
+        hash = 37 * hash + (int) (this.hashSetID ^ (this.hashSetID >>> 32));
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final HashSetFilter other = (HashSetFilter) obj;
+        if (!Objects.equals(this.hashSetName, other.hashSetName)) {
+            return false;
+        }
+        if (this.hashSetID != other.hashSetID) {
+            return false;
+        }
+        return isSelected() == other.isSelected();
+    }
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/HideKnownFilter.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/HideKnownFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..68acce984ce5f7f0cb2d63bd8da5d5594ac41c1b
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/HideKnownFilter.java
@@ -0,0 +1,61 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014-15 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.datamodel.timeline.filters;
+
+/**
+ * Filter to hide known files
+ */
+public class HideKnownFilter extends AbstractFilter {
+
+    @Override
+    public String getDisplayName() {
+        return BundleUtils.getBundle().getString("hideKnownFilter.displayName.text");
+    }
+
+    public HideKnownFilter() {
+        super();
+        selectedProperty().set(false);
+    }
+
+    @Override
+    public HideKnownFilter copyOf() {
+        HideKnownFilter hideKnownFilter = new HideKnownFilter();
+        hideKnownFilter.setSelected(isSelected());
+        hideKnownFilter.setDisabled(isDisabled());
+        return hideKnownFilter;
+    }
+
+    @Override
+    public int hashCode() {
+        return 7;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final HideKnownFilter other = (HideKnownFilter) obj;
+
+        return isSelected() == other.isSelected();
+    }
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/IntersectionFilter.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/IntersectionFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..35bb79a61537258313a75cead8916b74387f7aa1
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/IntersectionFilter.java
@@ -0,0 +1,80 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014-15 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.datamodel.timeline.filters;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import javafx.collections.FXCollections;
+
+/**
+ * Intersection (And) filter
+ */
+public class IntersectionFilter<S extends Filter> extends CompoundFilter<S> {
+
+	public IntersectionFilter(List<S> subFilters) {
+		super(subFilters);
+	}
+
+	public IntersectionFilter() {
+		super(FXCollections.<S>observableArrayList());
+	}
+
+	@Override
+	public IntersectionFilter<S> copyOf() {
+		@SuppressWarnings("unchecked")
+		IntersectionFilter<S> filter = new IntersectionFilter<>(
+				(List<S>) this.getSubFilters().stream()
+						.map(Filter::copyOf)
+						.collect(Collectors.toList()));
+		filter.setSelected(isSelected());
+		filter.setDisabled(isDisabled());
+		return filter;
+	}
+
+	@Override
+	public String getDisplayName() {
+		String collect = getSubFilters().stream()
+				.map(Filter::getDisplayName)
+				.collect(Collectors.joining(",", "[", "]"));
+		return BundleUtils.getBundle().getString("IntersectionFilter.displayName.text") + collect;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (obj == null) {
+			return false;
+		}
+		if (getClass() != obj.getClass()) {
+			return false;
+		}
+		@SuppressWarnings("unchecked")
+		final IntersectionFilter<S> other = (IntersectionFilter<S>) obj;
+
+		if (isSelected() != other.isSelected()) {
+			return false;
+		}
+
+		for (int i = 0; i < getSubFilters().size(); i++) {
+			if (getSubFilters().get(i).equals(other.getSubFilters().get(i)) == false) {
+				return false;
+			}
+		}
+		return true;
+	}
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/RootFilter.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/RootFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..f40101d44ea995d38657345ffd7797c907208d35
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/RootFilter.java
@@ -0,0 +1,140 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2015-16 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.datamodel.timeline.filters;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+import javafx.beans.binding.BooleanBinding;
+import javafx.collections.FXCollections;
+
+/**
+ * An implementation of IntersectionFilter designed to be used as the root of a
+ * filter tree. provides named access to specific subfilters.
+ */
+public class RootFilter extends IntersectionFilter<Filter> {
+
+    private final HideKnownFilter knownFilter;
+    private final TagsFilter tagsFilter;
+    private final HashHitsFilter hashFilter;
+    private final TextFilter textFilter;
+    private final TypeFilter typeFilter;
+    private final DataSourcesFilter dataSourcesFilter;
+
+    public DataSourcesFilter getDataSourcesFilter() {
+        return dataSourcesFilter;
+    }
+
+    public TagsFilter getTagsFilter() {
+        return tagsFilter;
+    }
+
+    public HashHitsFilter getHashHitsFilter() {
+        return hashFilter;
+    }
+
+    public TypeFilter getTypeFilter() {
+        return typeFilter;
+    }
+
+    public HideKnownFilter getKnownFilter() {
+        return knownFilter;
+    }
+
+    public TextFilter getTextFilter() {
+        return textFilter;
+    }
+
+    public RootFilter(HideKnownFilter knownFilter, TagsFilter tagsFilter, HashHitsFilter hashFilter, TextFilter textFilter, TypeFilter typeFilter, DataSourcesFilter dataSourceFilter, Set<Filter> annonymousSubFilters) {
+        super(FXCollections.observableArrayList(
+                textFilter,
+                knownFilter,
+                dataSourceFilter, tagsFilter,
+                hashFilter,
+                typeFilter
+        ));
+        this.knownFilter = knownFilter;
+        this.tagsFilter = tagsFilter;
+        this.hashFilter = hashFilter;
+        this.textFilter = textFilter;
+        this.typeFilter = typeFilter;
+        this.dataSourcesFilter = dataSourceFilter;
+        getSubFilters().addAll(annonymousSubFilters);
+        setSelected(Boolean.TRUE);
+        setDisabled(false);
+    }
+
+    @Override
+    public RootFilter copyOf() {
+        Set<Filter> annonymousSubFilters = getSubFilters().stream()
+                .filter(subFilter ->
+                        !(subFilter.equals(knownFilter)
+                        || subFilter.equals(tagsFilter)
+                        || subFilter.equals(hashFilter)
+                        || subFilter.equals(typeFilter)
+                        || subFilter.equals(textFilter)
+                        || subFilter.equals(dataSourcesFilter)))
+                .map(Filter::copyOf)
+                .collect(Collectors.toSet());
+
+        RootFilter filter = new RootFilter(
+                knownFilter.copyOf(),
+                tagsFilter.copyOf(),
+                hashFilter.copyOf(),
+                textFilter.copyOf(),
+                typeFilter.copyOf(),
+                dataSourcesFilter.copyOf(),
+                annonymousSubFilters);
+        filter.setSelected(isSelected());
+        filter.setDisabled(isDisabled());
+        return filter;
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode();
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        return areSubFiltersEqual(this, (CompoundFilter<Filter>) obj);
+    }
+
+    @Override
+    public boolean isActive() {
+        return true;
+    }
+
+    @Override
+    public BooleanBinding activeProperty() {
+
+        return new BooleanBinding() {
+            @Override
+            protected boolean computeValue() {
+                return true;
+            }
+        };
+    }
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/TagNameFilter.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/TagNameFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..70d4d277b6eb3207328a007f6b678a41e1aeedfe
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/TagNameFilter.java
@@ -0,0 +1,75 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2015 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.datamodel.timeline.filters;
+
+import java.util.Objects;
+import org.sleuthkit.datamodel.TagName;
+
+/**
+ * Filter for an individual TagName
+ */
+public class TagNameFilter extends AbstractFilter {
+
+	private final TagName tagName;
+
+	public TagNameFilter(TagName tagName) {
+		this.tagName = tagName;
+		setSelected(Boolean.TRUE);
+	}
+
+	public TagName getTagName() {
+		return tagName;
+	}
+
+	@Override
+	synchronized public TagNameFilter copyOf() {
+		TagNameFilter filterCopy = new TagNameFilter(getTagName());
+		filterCopy.setSelected(isSelected());
+		filterCopy.setDisabled(isDisabled());
+		return filterCopy;
+	}
+
+	@Override
+	public String getDisplayName() {
+		return tagName.getDisplayName();
+	}
+
+	@Override
+	public int hashCode() {
+		int hash = 3;
+		hash = 53 * hash + Objects.hashCode(this.tagName);
+		return hash;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (obj == null) {
+			return false;
+		}
+		if (getClass() != obj.getClass()) {
+			return false;
+		}
+		final TagNameFilter other = (TagNameFilter) obj;
+		if (!Objects.equals(this.tagName, other.tagName)) {
+			return false;
+		}
+
+		return isSelected() == other.isSelected();
+	}
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/TagsFilter.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/TagsFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..ab7d6aeb6b187a0992994ecd1c2d8f7cade7e9d9
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/TagsFilter.java
@@ -0,0 +1,90 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2015-16 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.datamodel.timeline.filters;
+
+import java.util.Comparator;
+import java.util.function.Predicate;
+import javafx.beans.binding.Bindings;
+import javafx.beans.value.ObservableBooleanValue;
+import org.sleuthkit.datamodel.TagName;
+
+/**
+ * Filter to show only events tag with the tagNames of the selected subfilters.
+ */
+public class TagsFilter extends UnionFilter<TagNameFilter> {
+
+    @Override
+    public String getDisplayName() {
+		return BundleUtils.getBundle().getString("tagsFilter.displayName.text");
+    }
+
+    public TagsFilter() {
+        setSelected(false);
+    }
+
+    @Override
+    public TagsFilter copyOf() {
+        TagsFilter filterCopy = new TagsFilter();
+        //add a copy of each subfilter
+        getSubFilters().forEach(tagNameFilter -> filterCopy.addSubFilter(tagNameFilter.copyOf()));
+        //these need to happen after the listeners fired by adding the subfilters 
+        filterCopy.setSelected(isSelected());
+        filterCopy.setDisabled(isDisabled());
+
+        return filterCopy;
+    }
+
+    @Override
+    public int hashCode() {
+        return 7;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final TagsFilter other = (TagsFilter) obj;
+
+        if (isActive() != other.isActive()) {
+            return false;
+        }
+
+        return areSubFiltersEqual(this, other);
+    }
+
+    public void removeFilterForTag(TagName tagName) {
+        getSubFilters().removeIf(subfilter -> subfilter.getTagName().equals(tagName));
+        getSubFilters().sort(Comparator.comparing(TagNameFilter::getDisplayName));
+    }
+
+    @Override
+    public ObservableBooleanValue disabledProperty() {
+        return Bindings.or(super.disabledProperty(), Bindings.isEmpty(getSubFilters()));
+    }
+
+    @Override
+    Predicate<TagNameFilter> getDuplicatePredicate(TagNameFilter subfilter) {
+        return tagNameFilter -> subfilter.getTagName().equals(tagNameFilter.getTagName());
+    }
+
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/TextFilter.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/TextFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..b00434f2c652570250c8dde006947267e8c21651
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/TextFilter.java
@@ -0,0 +1,86 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013-15 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.datamodel.timeline.filters;
+
+import java.util.Objects;
+import javafx.beans.property.Property;
+import javafx.beans.property.SimpleStringProperty;
+
+/**
+ * Filter for text matching
+ */
+public class TextFilter extends AbstractFilter {
+
+    public TextFilter() {
+    }
+
+    public TextFilter(String text) {
+        this.text.set(text);
+    }
+
+    private final SimpleStringProperty text = new SimpleStringProperty();
+
+    synchronized public void setText(String text) {
+        this.text.set(text);
+    }
+
+    @Override
+    public String getDisplayName() {
+		return BundleUtils.getBundle().getString("TextFilter.displayName.text");
+    }
+
+    synchronized public String getText() {
+        return text.getValue();
+    }
+
+    public Property<String> textProperty() {
+        return text;
+    }
+
+    @Override
+    synchronized public TextFilter copyOf() {
+        TextFilter textFilter = new TextFilter(getText());
+        textFilter.setSelected(isSelected());
+        textFilter.setDisabled(isDisabled());
+        return textFilter;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final TextFilter other = (TextFilter) obj;
+
+        if (isSelected() != other.isSelected()) {
+            return false;
+        }
+        return Objects.equals(text.get(), other.text.get());
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 5;
+        hash = 29 * hash + Objects.hashCode(this.text.get());
+        return hash;
+    }
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/TypeFilter.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/TypeFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..2e4c7833dbf7fd8c0dfc7790b545c8b26b892831
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/TypeFilter.java
@@ -0,0 +1,140 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2013-16 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.datamodel.timeline.filters;
+
+import java.util.Comparator;
+import java.util.Objects;
+import java.util.function.Predicate;
+import javafx.collections.FXCollections;
+import javafx.scene.image.Image;
+import javafx.scene.paint.Color;
+import org.sleuthkit.datamodel.timeline.EventType;
+import org.sleuthkit.datamodel.timeline.RootEventType;
+
+/**
+ * Event Type Filter. An instance of TypeFilter is usually a tree that parallels
+ * the event type hierarchy with one filter/node for each event type.
+ */
+public class TypeFilter extends UnionFilter<TypeFilter> {
+
+    static private final Comparator<TypeFilter> comparator = Comparator.comparing(TypeFilter::getEventType, EventType.getComparator());
+
+    /**
+     * the event type this filter passes
+     */
+    private final EventType eventType;
+
+    /**
+     * private constructor that enables non recursive/tree construction of the
+     * filter hierarchy for use in {@link TypeFilter#copyOf()}.
+     *
+     * @param et        the event type this filter passes
+     * @param recursive true if subfilters should be added for each subtype.
+     *                  False if no subfilters should be added.
+     */
+    private TypeFilter(EventType et, boolean recursive) {
+        super(FXCollections.observableArrayList());
+        this.eventType = et;
+
+        if (recursive) { // add subfilters for each subtype
+            for (EventType subType : et.getSubTypes()) {
+                addSubFilter(new TypeFilter(subType), comparator);
+            }
+        }
+    }
+
+    /**
+     * public constructor. creates a subfilter for each subtype of the given
+     * event type
+     *
+     * @param et the event type this filter will pass
+     */
+    public TypeFilter(EventType et) {
+        this(et, true);
+    }
+
+    public EventType getEventType() {
+        return eventType;
+    }
+
+    @Override
+    public String getDisplayName() {
+        return (eventType == RootEventType.getInstance())
+                ? BundleUtils.getBundle().getString("TypeFilter.displayName.text")
+                : eventType.getDisplayName();
+    }
+
+    /**
+     * @return a color to use in GUI components representing this filter
+     */
+    public Color getColor() {
+        return eventType.getColor();
+    }
+
+    /**
+     * @return an image to use in GUI components representing this filter
+     */
+    public Image getFXImage() {
+        return eventType.getFXImage();
+    }
+
+    @Override
+    public TypeFilter copyOf() {
+        //make a nonrecursive copy of this filter
+        final TypeFilter filterCopy = new TypeFilter(eventType, false);
+        //add a copy of each subfilter
+        getSubFilters().forEach(typeFilter -> filterCopy.addSubFilter(typeFilter.copyOf(), comparator));
+        //these need to happen after the listeners fired by adding the subfilters 
+        filterCopy.setSelected(isSelected());
+        filterCopy.setDisabled(isDisabled());
+        return filterCopy;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final TypeFilter other = (TypeFilter) obj;
+
+        if (isActive() != other.isActive()) {
+            return false;
+        }
+
+        if (this.eventType != other.eventType) {
+            return false;
+        }
+        return areSubFiltersEqual(this, other);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        hash = 67 * hash + Objects.hashCode(this.eventType);
+        return hash;
+    }
+
+    @Override
+    Predicate<TypeFilter> getDuplicatePredicate(TypeFilter subfilter) {
+        return t -> subfilter.getEventType().equals(t.eventType);
+    }
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/UnionFilter.java b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/UnionFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..5faeb153a6a60529098e90a573b73eaadf6a8d7b
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/timeline/filters/UnionFilter.java
@@ -0,0 +1,52 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2014-16 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.datamodel.timeline.filters;
+
+import java.util.Comparator;
+import java.util.function.Predicate;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+/**
+ * Union(or) filter
+ */
+abstract public class UnionFilter<SubFilterType extends Filter> extends CompoundFilter<SubFilterType> {
+
+    public UnionFilter(ObservableList<SubFilterType> subFilters) {
+        super(subFilters);
+    }
+
+    public UnionFilter() {
+        super(FXCollections.<SubFilterType>observableArrayList());
+    }
+
+    abstract Predicate<SubFilterType> getDuplicatePredicate(SubFilterType subfilter);
+
+    public void addSubFilter(SubFilterType subfilter) {
+        addSubFilter(subfilter, Comparator.comparing(SubFilterType::getDisplayName));
+    }
+
+    protected void addSubFilter(SubFilterType subfilter, Comparator<SubFilterType> comparator) {
+        Predicate<SubFilterType> duplicatePredicate = getDuplicatePredicate(subfilter);
+        if (getSubFilters().stream().anyMatch(duplicatePredicate) == false) {
+            getSubFilters().add(subfilter);
+        }
+        getSubFilters().sort(comparator);
+    }
+}