diff --git a/Makefile.am b/Makefile.am
index 42d3432c4a00d9bc792d4afe71c03a6e758f9d71..bc377efc069939e44537fdf9f6c0bd2e5afbf28a 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1,6 +1,7 @@
 # File that we want to include in the dist
 EXTRA_DIST = README_win32.txt README.md INSTALL.txt ChangeLog.txt NEWS.txt API-CHANGES.txt \
-    licenses/GNU-COPYING licenses/IBM-LICENSE licenses/cpl1.0.txt \
+    licenses/GNU-COPYING licenses/IBM-LICENSE \
+    licenses/cpl1.0.txt licenses/bsd.txt licenses/mit.txt \
     m4/*.m4 \
     docs/README.txt \
     packages/sleuthkit.spec \
diff --git a/NEWS.txt b/NEWS.txt
index eca0bddd06598f03f3a9b4cf09906833ef2a79a8..2cbd1d7b2f3ef8888a30376d618d9bca41248c3d 100644
--- a/NEWS.txt
+++ b/NEWS.txt
@@ -1,6 +1,17 @@
 ---------------- VERSION 4.9.0 --------------
 C/C++
-- Removed framework project.  Use Autopsy instead....
+- Removed framework project.  Use Autopsy instead if you need an analysis framework. 
+- Various fixes from Google-based fuzzing.
+- Ensure all reads (even big ones) are sector aligned when reading from Windows device.
+- Ensure all command line tools support new pool command line arguments. 
+- Create virtual files for APFS unallocated space
+- HFS fix to display type
+
+Java:
+- More artifact helper methods
+- More artifacts and attributes for drones and GPS coordinates
+- Updated TimelineManager to insert GPS artifacts into events table
+
 
 ---------------- VERSION 4.8.0 --------------
 C/C++
diff --git a/README.md b/README.md
index 63e0a4853fb34428649d19bcfad00fdbc8bf5452..ff66ccf0beae568ebcbe1a3db96d3eb3429e7ff4 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,6 @@
 [![Build status](https://ci.appveyor.com/api/projects/status/8f7ljj8s2lh5sqfv?svg=true)](https://ci.appveyor.com/project/bcarrier/sleuthkit)
 
 # [The Sleuth Kit](http://www.sleuthkit.org/sleuthkit)
-README File
 
 ## INTRODUCTION
 The Sleuth Kit is an open source forensic toolkit for analyzing
@@ -16,8 +15,8 @@ the tool or customize it to specific needs.
 The Sleuth Kit uses code from the file system analysis tools of
 The Coroner's Toolkit (TCT) by Wietse Venema and Dan Farmer.  The
 TCT code was modified for platform independence.  In addition,
-support was added for the NTFS (see docs/ntfs.README) and FAT (see
-docs/fat.README) file systems.  Previously, The Sleuth Kit was
+support was added for the NTFS (see [wiki/ntfs](http://wiki.sleuthkit.org/index.php?title=FAT_Implementation_Notes)) 
+and FAT (see [wiki/fat](http://wiki.sleuthkit.org/index.php?title=NTFS_Implementation_Notes)) file systems.  Previously, The Sleuth Kit was
 called The @stake Sleuth Kit (TASK).  The Sleuth Kit is now independent
 of any commercial or academic organizations.
 
@@ -35,8 +34,8 @@ The Sleuth Kit allows one to analyze a disk or file system image
 created by 'dd', or a similar application that creates a raw image.
 These tools are low-level and each performs a single task.  When
 used together, they can perform a full analysis.  For a more detailed
-description of these tools, refer to docs/filesystem.README.  The
-tools are briefly described in a file system layered approach.  Each
+description of these tools, refer to [wiki/filesystem](http://wiki.sleuthkit.org/index.php?title=TSK_Tool_Overview). 
+The tools are briefly described in a file system layered approach.  Each
 tool name begins with a letter that is assigned to the layer.  
 
 ### File System Layer:
@@ -88,8 +87,8 @@ contents of the data units allocated to the metadata structure
 which metadata structure has allocated a given content unit or
 file name.
 
-Refer to the ntfs.README doc for information on addressing metadata
-attributes in NTFS.
+Refer to the [ntfs wiki](http://wiki.sleuthkit.org/index.php?title=NTFS_Implementation_Notes) 
+for information on addressing metadata attributes in NTFS.
 
 ### Human Interface Layer (file):
 The human interface layer allows one to interact with files in a
@@ -110,7 +109,7 @@ made.  The mactime (TCT) program takes as input the 'body' file
 that was generated by fls and ils.  To get data on allocated and
 unallocated file names, use 'fls -rm dir' and for unallocated inodes
 use 'ils -m'.  Note that the behavior of these tools are different
-than in TCT.  For more information, refer to docs/mac.README.
+than in TCT.  For more information, refer to [wiki/mactime](http://wiki.sleuthkit.org/index.php?title=Mactime).
 
 
 #### Hash Databases
@@ -127,7 +126,7 @@ an index of a hash database and perform quick lookups using a binary
 search algorithm.  The 'hfind' tool can perform lookups on the NIST
 National Software Reference Library (NSRL) (www.nsrl.nist.gov) and
 files created from the 'md5' or 'md5sum' command.   Refer to the 
-docs/hfind.README file for more details.  
+[wiki/hfind](http://wiki.sleuthkit.org/index.php?title=Hfind) file for more details.  
 
 #### File Type Categories
 Different types of files typically have different internal structure.
@@ -143,24 +142,33 @@ The 'sorter' program in The Sleuth Kit will use other Sleuth Kit
 tools to sort the files in a file system image into categories.
 The categories are based on rule sets in configuration files.  The
 'sorter' tool will also use hash databases to flag known bad files
-and ignore known good files.  Refer to the 'docs/sorter.README'
+and ignore known good files.  Refer to the [wiki/sorter](http://wiki.sleuthkit.org/index.php?title=Sorter)
 file for more details.
 
 
 ## LICENSE
-The file system tools (in the src/fstools directory) are released
-under the IBM open source license and Common Public License, both
-are located in the license directory.  The modifications to 'mactime'
-from the original 'mactime' in TCT and 'mac-daddy' are released
-under the Common Public License.  Other tools in the src directory
-are either Common Public License or the GNU Public License.
+There are a variety of licenses used in TSK based on where they
+were first developed.  The licenses are located in the [licenses
+directory](https://github.com/sleuthkit/sleuthkit/tree/develop/licenses).
+
+- The file system tools (in the
+[tools/fstools](https://github.com/sleuthkit/sleuthkit/tree/develop/tools/fstools)
+directory) are released under the IBM open source license and Common
+Public License.
+- srch_strings and fiwalk are released under the GNU Public License
+- Other tools in the tools directory are Common Public License
+- The modifications to 'mactime' from the original 'mactime' in TCT
+and 'mac-daddy' are released under the Common Public License.
+
+The library uses utilities that were released under MIT and BSD 3-clause. 
+
 
 ## INSTALL
 For installation instructions, refer to the INSTALL.txt document.
 
 ## OTHER DOCS
-The 'docs' directory contains documents that describe the provided tools
-in more detail.  The Sleuth Kit Informer is a newsletter that contains
+The [wiki](http://wiki.sleuthkit.org/index.php?title=Main_Page) contains documents that 
+describe the provided tools in more detail.  The Sleuth Kit Informer is a newsletter that contains
 new documentation and articles.
 
 > www.sleuthkit.org/informer/
@@ -172,5 +180,6 @@ announcements list.
 > http://sourceforge.net/mail/?group_id=55685
 
 Brian Carrier
-carrier <at> sleuthkit <dot> org
+
+carrier at sleuthkit dot org
  
diff --git a/bindings/java/build.xml b/bindings/java/build.xml
index 9a5350cfd153cd19753dc060f86492feba816109..57499ee96a2c2857ec5e8f0d17c0deb256c3d7e5 100644
--- a/bindings/java/build.xml
+++ b/bindings/java/build.xml
@@ -11,7 +11,7 @@
 	<import file="build-${os.family}.xml"/>
 
     <!-- Careful changing this because release-windows.pl updates it by pattern -->
-<property name="VERSION" value="4.8.0"/>
+<property name="VERSION" value="4.9.0"/>
 
 	<!-- set global properties for this build -->
 	<property name="default-jar-location" location="/usr/share/java"/>
diff --git a/bindings/java/doxygen/Doxyfile b/bindings/java/doxygen/Doxyfile
index b6843f0435e79ba289103d046a0a2da9c0957941..7c615e85573d81cd944dec2b9eba95450701705b 100644
--- a/bindings/java/doxygen/Doxyfile
+++ b/bindings/java/doxygen/Doxyfile
@@ -39,7 +39,7 @@ PROJECT_NAME           = "Sleuth Kit Java Bindings (JNI)"
 # control system is used.
 
 # NOTE: This is updated by the release-unix.pl script
-PROJECT_NUMBER         = 4.8.0 
+PROJECT_NUMBER = 4.9.0
 
 # Using the PROJECT_BRIEF tag one can provide an optional one line description
 # for a project that appears at the top of each page and should give viewer a
@@ -1050,7 +1050,7 @@ GENERATE_HTML          = YES
 # This tag requires that the tag GENERATE_HTML is set to YES.
 
 # NOTE: This is updated by the release-unix.pl script
-HTML_OUTPUT            = jni-docs/4.8.0/
+HTML_OUTPUT = jni-docs/4.9.0/
 
 # The HTML_FILE_EXTENSION tag can be used to specify the file extension for each
 # generated HTML page (for example: .htm, .php, .asp).
diff --git a/bindings/java/doxygen/artifact_catalog.dox b/bindings/java/doxygen/artifact_catalog.dox
index 358bd2558b0a7e7eb33f026539ba79f93ee6ac3d..3caab82d2fd4153f1e4208feb249f154977a9899 100644
--- a/bindings/java/doxygen/artifact_catalog.dox
+++ b/bindings/java/doxygen/artifact_catalog.dox
@@ -2,11 +2,19 @@
 
 
 # Introduction
-This document reflects current standard usage of artifacts and attributes for posting analysis results to the case blackboard.
-
-Note that "TSK" is an abbreviation of "The SleuthKit." Artifact type names with a "TSK_" prefix indicate the names of standard or "built in" types. User-defined artifact and attribute types should not be given names with "TSK_" prefixes.
+This document reflects current standard usage of artifact and attribute types for posting analysis results to the case blackboard in Autopsy.  Refer to \ref mod_bbpage for more background on the blackboard and how to make artifacts. 
 
 The catalog section below has one entry for each standard artifact type. Each entry lists the required and optional attributes of artifacts of the type.
+
+NOTE:
+- While we have listed some attributes as "Required", nothing will enforce that they exist. Modules that use artifacts from the blackboard should assume that some of the attributes may not actually exist. 
+- You are not limited to the attributes listed below for each artifact.  Attributes are listed below as "Optional" if at least one, but not all, Autopsy modules create them.  If you want to store data that is not listed below, use an existing attribute type or make your own.  
+
+For the full list of types, refer to:
+- org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE
+- org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE
+
+
 <h1>Artifacts Catalog</h1>
 
 In alphabetical order.
@@ -26,6 +34,8 @@ TSK_CARD_NUMBER (Credit card number)
 - TSK_SET_NAME (The keyword list name, i.e., "Credit Card Numbers", when the account is a credit card discovered by the Autopsy regular expression search for credit cards)
 
 ---
+
+
 ## TSK_ASSOCIATED_OBJECT
 Provides a backwards link to an artifact that references the parent file of this artifact.  Example usage is that a downloaded file will have this artifact and it will point back to the TSK_WEB_DOWNLOAD artifact that is associated with a browser's SQLite database. See \ref jni_bb_associated_object.
 
@@ -226,12 +236,13 @@ None
 
 ---
 ## TSK_GEN_INFO
-A generic information artifact.
+A generic information artifact. Each content object will have at most one TSK_GEN_INFO artifact, which is easily accessed through org.sleuthkit.datamodel.AbstractContent.getGenInfoArtifact() and related methods. The TSK_GEN_INFO object is useful for storing values related to the content object without making a new artifact type.
 
 ### REQUIRED ATTRIBUTES
 None
 
-
+### OPTIONAL ATTRIBUTES
+- TSK_PHOTODNA_HASH (The PhotoDNA hash of an image)
 
 ---
 ## TSK_GPS_BOOKMARK
@@ -271,10 +282,7 @@ The last known location of a GPS connected device. This may be from a perspectiv
 A GPS route.
 
 ### REQUIRED ATTRIBUTES
-- TSK_GEO_LATITUDE_START (The latitude value of the starting point)
-- TSK_GEO_LATITUDE_END (The latitude value of the ending point)
-- TSK_GEO_LONGITUDE_START (The longitude value of the ending point)
-- TSK_GEO_LONGITUDE_END (The longitude value of the ending point)
+- TSK_GEO_WAYPOINTS (JSON list of waypoints. Use org.sleuthkit.datamodel.blackboardutils.attributes.GeoWaypoints class to create/process)
 
 ### OPTIONAL ATTRIBUTES
 - TSK_DATETIME (Timestamp of the GPS route, in seconds since 1970-01-01T00:00:00Z)
@@ -301,18 +309,15 @@ A GPS location that was known to have been searched by the device or user.
 
 
 ---
-## TSK_GPS_TRACKPOINT
-A GPS trackpoint found in an application, file or database.
+## TSK_GPS_TRACK
+A Global Positioning System (GPS) track artifact records the track, or path, of a GPS-enabled dvice as a connected series of track points. A track point is a location in a geographic coordinate system with latitude, longitude and altitude (elevation) axes.
 
 ### REQUIRED ATTRIBUTES
-- TSK_GEO_LATITUDE (The GPS latitude value that was tracked)
-- TSK_GEO_LONGITUDE (The GPS longitude value that was tracked)
+- TSK_GEO_TRACKPOINTS (JSON list of trackpoints. Use org.sleuthkit.datamodel.blackboardutils.attributes.GeoTrackPoints class to create/process)
 
 ### OPTIONAL ATTRIBUTES
-- TSK_DATETIME (Timestamp of the GPS trackpoint, in seconds since 1970-01-01T00:00:00Z)
-- TSK_GEO_ALTITUDE (Altitude of the latitude and longitude values)
-- TSK_NAME (The name of the trackpoint. Ex: Boston)
-- TSK_PROG_NAME (Name of application containing the GPS trackpoint)
+- TSK_NAME (The name of the trackpoint set. Ex: Boston)
+- TSK_PROG_NAME (Name of application containing the GPS trackpoint set)
 
 
 
@@ -396,6 +401,7 @@ A message that is found in some content.
 - TSK_MESSAGE_TYPE (E.g., WhatsApp Message, Skype Message, etc.)
 
 ### OPTIONAL ATTRIBUTES
+- TSK_ATTACHMENTS (Attachments - use the org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper class to add an attachment)
 - TSK_DATETIME (Timestamp the message was sent or received, in seconds since 1970-01-01T00:00:00Z) 
 - TSK_DIRECTION (Direction of the message, e.g., incoming or outgoing)
 - TSK_EMAIL_FROM (Email address of the sender)
@@ -519,7 +525,8 @@ Indicates recently accessed content. Examples: Recent Documents or Recent Downlo
 - TSK_PROG_NAME (Application or application extractor that stored this object as recent)
 - TSK_DATETIME (A timestamp associated with the content, in seconds since 1970-01-01T00:00:00Z. Ex: creation time)
 - TSK_NAME (If found in the registry, the name of the attribute)
-- TSK_VALUE(If found in the registry, the value of the attribute)
+- TSK_VALUE (If found in the registry, the value of the attribute)
+- TSK_COMMENT (What the source of the attribute may be)
 
 
 
diff --git a/bindings/java/doxygen/blackboard.dox b/bindings/java/doxygen/blackboard.dox
index 7fb38c7571eb28933e11b11d847fe7278c95b0cf..68c4ee1dabbbf1a5bd2980594c5c45bce2d6cc05 100644
--- a/bindings/java/doxygen/blackboard.dox
+++ b/bindings/java/doxygen/blackboard.dox
@@ -6,30 +6,35 @@ The blackboard allows modules (in Autopsy or other frameworks) to communicate an
 
 \subsection jni_bb_concepts Concepts
 
-The blackboard is a collection of <em>artifacts</em>.  Each artifact has a type, such as web browser history, EXIF, or GPS track points. The Sleuth Kit has many artifact types already defined (see org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE and the \ref artifact_catalog_page "artifact catalog") and you can also \ref jni_bb_artifact2 "create your own". 
+The blackboard is a collection of <em>artifacts</em>.  Each artifact has a type, such as web browser history, EXIF, or GPS route. The Sleuth Kit has many artifact types already defined (see org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE and the \ref artifact_catalog_page "artifact catalog") and you can also \ref jni_bb_artifact2 "create your own". 
 
-Each artifact has a set of name-value pairs called <em>attributes</em>.  Attributes also have types, such as URL, Created Date, or Device Make. The Sleuth Kit has many attribute types already defined (see org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE) and you can also create your own.  See the \ref artifact_catalog_page "artifact catalog" for a list of artifacts and the attributes that should be associated with each.
+Each artifact has a set of name-value pairs called <em>attributes</em>.  Attributes also have types, such as URL, created date, or device make. The Sleuth Kit has many attribute types already defined (see org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE) and you can also \ref jni_bb_artifact2 "create your own".  
 
-When a module wants to store its results in the blackboard, it makes an artifact of the correct type and then adds attributes to it. Other modules can then query the blackboard for artifacts of a given type or artifacts associated with a given file. 
+See the \ref artifact_catalog_page "artifact catalog" for a list of artifacts and the attributes that should be associated with each.
 
-There are two special types of artifacts that are used a bit differently than the rest. The first is the org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_GEN_INFO artifact.  A Content object should have only one artifact of this type and it is used to store single attributes that are not related to each other and that do not need their own artifact. There are special methods to access this artifact to ensure that only a single TSK_GEN_INFO artifact is created per Content object and that you get a cached version of the artifact. These methods will be given in the relevant sections below.
+\subsection jni_bb_specialart Special Artifact Types
+
+There are two special types of artifacts that are used a bit differently than the rest. 
+
+The first is the org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_GEN_INFO artifact.  A Content object should have only one artifact of this type and it is used to store a independent attributes that will not be displayed in the UI. Autopsy used to store the MD5 hash and MIME type in TSK_GEN_INFO, but they are now in the files table of the database. There are special methods to access this artifact to ensure that only a single TSK_GEN_INFO artifact is created per Content object and that you get a cached version of the artifact. These methods will be given in the relevant sections below.
 
 The second special type of artifact is the TSK_ASSOCIATED_OBJECT. All artifacts are created as the child of a file or artifact. This TSK_ASSOCIATED_OBJECT is used to make additional relationships with files and artifacts apart from this parent-child relationship. See the \ref jni_bb_associated_object section below.
 
 \section jni_bb_access Accessing the Blackboard
 
-Java modules can access the blackboard from either org.sleuthkit.datamodel.SleuthkitCase or a org.sleuthkit.datamodel.Content object.  The methods associated with org.sleuthkit.datamodel.Content all limit the Blackboard to a specific file.
+Modules can access the blackboard from either org.sleuthkit.datamodel.SleuthkitCase or a org.sleuthkit.datamodel.Content object.  The methods associated with org.sleuthkit.datamodel.Content all limit the Blackboard to a specific file.
 
 \subsection jni_bb_access_post Posting to the Blackboard
 
 The first thing you need to do is create the artifact.  All artifacts must be associated with a Content object.  You can do this by creating an instance of org.sleuthkit.datamodel.BlackboardArtifact by calling either:
 - org.sleuthkit.datamodel.Content.newArtifact(BlackboardArtifact.ARTIFACT_TYPE type) on the Content object you are adding the artifact to
-- org.sleuthkit.datamodel.SleuthkitCase.newBlackboardArtifact(ARTIFACT_TYPE artifactType, long obj_id) or a variation.  This approach is usually taken if you don’t have a Content object already and don’t want to create one just to make an artifact. 
+- org.sleuthkit.datamodel.SleuthkitCase.newBlackboardArtifact(ARTIFACT_TYPE artifactType, long obj_id) or a variation.
+
 With either of these approaches, the artifact is created in the database immediately. 
 
 If you want to create an attribute in the TSK_GEN_INFO artifact, use org.sleuthkit.datamodel.Content.getGenInfoArtifact() to ensure that you do not create a second TSK_GEN_INFO artifact for the file and to ensure that you used the cached version (which will be faster for you). 
 
-Next, you need to make attributes and add them to the artifact.  Attributes are created by making a new instance of org.sleuthkit.datamodel.BlackboardAttribute using one of the various constructors. After you create one with the correct type and value, you add it to the artifact using org.sleuthkit.datamodel.BlackboardArtifact.addAttribute() (or org.sleuthkit.datamodel.BlackboardArtifact.addAttributes() if you have several to add - it’s faster). 
+Next, you need to make attributes and add them to the artifact.  Attributes are created by making a new instance of org.sleuthkit.datamodel.BlackboardAttribute using one of the various constructors. After you create one with the correct type and value, you add it to the artifact using org.sleuthkit.datamodel.BlackboardArtifact.addAttribute() (or org.sleuthkit.datamodel.BlackboardArtifact.addAttributes() if you have several to add - it’s faster). Note that you should not manually add attributes of type JSON for standard attribute types such as TSK_ATTACHMENTS or TSK_GEO_TRACKPOINTS. Instead, you should use the helper classes in org.sleuthkit.datamodel.blackboardutils.attributes or org.sleuthkit.datamodel.blackboardutils to create your artifacts.
 
 \subsubsection jni_bb_artifact2 Creating Multiple Artifacts or Multiple Attributes
 
@@ -47,14 +52,13 @@ These classes provide methods that abstract the details of artifacts and attribu
 The following helpers are available:
 
 <ul>
-<li>org.sleuthkit.datamodel.blackboardutils.ArtifactsHelper - provides methods for some creating some general artifacts
+<li>org.sleuthkit.datamodel.blackboardutils.ArtifactsHelper - provides methods for creating general artifacts
 <ul>
-<li>addGPSLocation(): creates TSK_GPS_TRACKPOINT artifact
 <li>addInstalledPrograms(): creates TSK_INSTALLED_PROG artifact
 </ul></ul>
 
 <ul>
-<li>org.sleuthkit.datamodel.blackboardutils.WebBrowserArtifactsHelper - provides methods for some creating web browser related artifacts
+<li>org.sleuthkit.datamodel.blackboardutils.WebBrowserArtifactsHelper - provides methods for creating web browser related artifacts
 <ul>
 <li>addWebBookmark(): creates TSK_WEB_BOOKMARK artifact for browser bookmarks
 <li>addWebCookie(): creates TSK_WEB_COOKIE artifact for browser cookies
@@ -73,36 +77,219 @@ The following helpers are available:
 <li>addAttachments() adds attachments to a message.
 </ul></ul>
 
+<ul>
+<li>org.sleuthkit.datamodel.blackboardutils.GeoArtifactsHelper - provides methods for GPS related artifacts
+<ul>
+<li>addRoute(): creates TSK_ROUTE artifact for GPS routes.
+<li>addContact() creates TSK_CONTACT artifact for contacts.
+<li>addMessage() creates a TSK_MESSAGE artifact for messages.
+<li>addAttachments() adds attachments to a message.
+</ul></ul>
+
 \subsubsection jni_bb_associated_object Associated Objects
 
 Artifacts should be created as children of the file that they were derived or parsed from. For example, a TSK_WEB_DOWNLOAD artifact would be a child of the browser's SQLite database that was parsed. This creates a relationship between the source file and the artifact. But, sometimes you also want to make a relationship between the artifact and another file (or artifact). This is where the TSK_ASSOCIATED_OBJECT artifact comes in.
 
-For example, suppose you have a module that parses a SQLite database that has a log of downloaded files. Each entry might contain the URL the file was downloaded from, timestamp information, and the location the file was saved to on disk. This data would be saved in a TSK_WEB_DOWNLOAD artifact that would be a child of the SQLite database. But suppose the downloaded file also exists in our image. It would be helpful to link that file to our TSK_WEB_DOWNLOAD artifact to show when and where it was download from.
+For example, suppose you have a module that parses a SQLite database that has a log of downloaded files. Each entry might contain the URL the file was downloaded from, timestamp information, and the location the file was saved to on disk. This data would be saved in a TSK_WEB_DOWNLOAD artifact that would be a child of the SQLite database. But suppose the downloaded file also exists in our image. It would be helpful to link that file to our TSK_WEB_DOWNLOAD artifact to show when and where it was downloaded from.
 
-We achieve this relationship by creating a TSK_ASSOCIATED_OBJECT artifact on the downloaded file. This artifact stores the ID of the TSK_WEB_DOWNLOAD artifact in TSK_ASSOCIATED_ARTIFACT attribute so we have a direct link from the file to the artifact that shows where it came from.
+We achieve this relationship by creating a TSK_ASSOCIATED_OBJECT artifact on the downloaded file. This artifact stores the ID of the TSK_WEB_DOWNLOAD artifact in a TSK_ASSOCIATED_ARTIFACT attribute so we have a direct link from the file to the artifact that shows where it came from.
  
 \image html associated_object.png
  
 \subsection jni_bb_query  Querying the Blackboard
 
-You can find artifacts using a variety of ways:
+You can find artifacts  using a variety of ways:
 - org.sleuthkit.datamodel.Content.getArtifacts() in its various forms to get a specific type of artifact for a specific Content object. 
-- org.sleuthkit.datamodel.Content.getGenInfoArtifact() to get the TSK_GEN_INFO artifact.
-- org.sleuthkit.datamodel.SleuthkitCase.getBlackboardArtifacts() in its various forms to get all artifacts of a given type (regardless of file it is associated with) or for a given file.  
-
+- org.sleuthkit.datamodel.Content.getGenInfoArtifact() to get the TSK_GEN_INFO artifact for a specific content object.
+- org.sleuthkit.datamodel.SleuthkitCase.getBlackboardArtifacts() in its various forms to get artifacts based on some combination of artifact type, attribute type and value, and content object.
 
 \section jni_bb_custom_types Custom Artifacts and Attributes
 
 This section outlines how to create artifact and attribute types because the standard ones do not meet your needs. These custom artifacts will be displayed
-in the Autopsy UI alongside the built in artifacts and will also appear in the reports. However, before you make a custom type, you should consider the 
-TSK_INTERESTING_FILE_HIT artifact.  It is very generic and we have used it 
-in the past when we did not want to make a new artifact type. You create the artifact, use the TSK_SET_NAME attribute to define the equivalent name of the custom artifact that you wanted to create, and then add whatever attributes  you want. 
+in the Autopsy UI alongside the built in artifacts and will also appear in the reports. 
 
 
 \subsection jni_bb_custom_make Making Custom Artifacts and Attributes
+
+
 org.sleuthkit.datamodel.SleuthkitCase.addBlackboardArtifactType() is used to create a custom artifact.  Give it the display and unique name and it will return a org.sleuthkit.datamodel.BlackboardArtifact.Type object with a unique ID.  You will need to call this once for each case to create the artifact ID.   You can then use this ID to make an artifact of the given type.  To check if the artifact type has already been added to the blackboard or to get the ID after it was created, use org.sleuthkit.datamodel.SleuthkitCase.getArtifactType().
 
-To create custom attributes, use org.sleuthkit.datamodel.SleuthkitCase.addArtifactAttributeType() to create the type and get its ID. Like artifacts, you must create the type for each new case. To get a type after it has been created in the case, use org.sleuthkit.datamodel.SleuthkitCase.getAttributeType(). 
+To create custom attributes, use org.sleuthkit.datamodel.SleuthkitCase.addArtifactAttributeType() to create the artifact type and get its ID. Like artifacts, you must create the attribute type for each new case. To get a type after it has been created in the case, use org.sleuthkit.datamodel.SleuthkitCase.getAttributeType(). Your attribute will be a name-value pair where the value is of the type you specified when creating it. The current types are: String, Integer, Long, Double, Byte, Datetime, and JSON. If you believe you need to create an attribute with type JSON, please read the 
+\ref jni_bb_json_attr_overview "overview" and \ref jni_bb_json_attr "tutorial" sections below. 
+
+Note that "TSK" is an abbreviation of "The Sleuth Kit." Artifact and attribute type names with a "TSK_" prefix indicate the names of standard or "built in" types. User-defined artifact and attribute types should not be given names with "TSK_" prefixes.
+
+\subsection jni_bb_json_attr_overview JSON Attribute Overview
+
+This section will give a quick overview of how to use JSON attributes. If this is your first time using JSON attributes please read the \ref jni_bb_json_attr below as well.
+
+\subsubsection jni_bb_json_attr_overview_usage JSON Attribute Usage
+
+Attributes with values of type JSON should be used only when the data can't be stored as an unordered set of attributes. To date, the most common need for this has been where an artifact needs to store multiple ordered instances of the same type of data in a single artifact. For example, one of the standard JSON attributes is TSK_GEO_TRACKPOINTS which stores an ordered list of track points, each containing coordinates, a timestamp, and other data. 
+
+\subsubsection jni_bb_json_attr_overview_format JSON Attribute Format
+
+The underlying data in a JSON attribute will be either an array of individual attributes or an array of maps of attributes. For example, an artifact containing two track points could look similar to this (some attributes have been removed for brevity):
+
+\verbatim
+{"pointList":
+  [
+    {"TSK_DATETIME":1255822646,
+     "TSK_GEO_LATITUDE":47.644548,
+     "TSK_GEO_LONGITUDE":-122.326897},
+    {"TSK_DATETIME":1255822651,
+     "TSK_GEO_LATITUDE":47.644548,
+     "TSK_GEO_LONGITUDE":-122.326897}
+  ]
+}
+\endverbatim
+
+In practice you will not be required to deal with the raw JSON, but it is important to note that in the name/value pairs, the name should always be the name of a blackboard artifact type. This allows Autopsy to better process each attribute, for example by displaying timestamps in human-readable format.
+
+\subsubsection jni_bb_json_attr_overview_create Saving JSON Attributes
+
+To start, follow the instructions in the \ref jni_bb_custom_make section above to create your custom attribute with value type JSON. Next you'll need to put your data into the new attribute. There are two general methods:
+
+<ol><li>Manually create the JSON string. This is not recommended as the code will be hard to read and prone to errors.
+<li> Create a helper plain old Java object (POJO) to hold the data you want to serialize.
+</ol>
+
+Assuming you go the POJO route (highly recommended), there are two options for creating your class. As discussed above, each field name should match an attribute name (either built-in or custom). You could create a class like this:
+
+\verbatim
+class WebLogEntry {
+   long TSK_DATETIME;
+   String TSK_URL;
+\endverbatim
+
+The downside here is that your code will likely be a bit less readable like this. The other option is to use annotations specifying which attribute type goes with each of your fields, like this:
+
+\verbatim
+class WebLogEntry {
+   @SerializedName("TSK_DATETIME")
+   long accessDate;
+   @SerializedName("TSK_URL")
+   String urlVisited;
+\endverbatim
+
+You may need to make multiple POJOs to hold the data you need to serialize. This would most commonly happen if you want to store a list of values. In our example above, we would likely need to create a WebLog class to hold our list of WebLogEntry objects.
+
+Now we need to convert our object into a JSON attribute. The easiest way to do this using the method org.sleuthkit.datamodel.blackboardutils.attributes.BlackboardJsonAttrUtil.toAttribute(). This method will return a BlackboardAttribute serialized from your object. You can then add this new attribute to your BlackboardArtifact.
+
+\subsubsection jni_bb_json_attr_overview_load Loading JSON Attributes
+
+If you need to process JSON attributes you created and you created your own POJO as discussed in the previous section, you can use the method org.sleuthkit.datamodel.blackboardutils.attributes.BlackboardJsonAttrUtil.fromAttribute(). It will return an instance of your class containing the data from a given BlackboardAttribute.
+
+\subsection jni_bb_json_attr JSON Attribute Tutorial
+
+The following describes an example of when you might need a JSON-valued attribute and the different methods for creating one. It also shows generally how to create custom artifacts and attributes so may be useful even if you do not need a JSON-type attribute.
+
+Suppose we had a module that could record the last few times an app was accessed and which user opened it. The data we'd like to store for one app could have the form:
+
+\verbatim
+App name: Sample App
+Logins:   user1, 2020-03-31 10:06:37 EDT
+          user2, 2020-03-30 06:19:57 EDT
+          user1, 2020-03-26 18:59:57 EDT
+\endverbatim
+
+We could make a separate artifact for each of those logins (each with the app name, user name, and timestamp) it might be nicer to have them all under one and keep them in order. This is where the JSON-type attribute comes into play. We can store all the login data in a single blackboard attribute.
+
+To start, we'll need to create our new artifact and attribute types. We'll need a new artifact type to hold our login data and a new attribute type to hold the logins themselves (this will be our JSON attribute). We'll use a standard attribute later for the app name. This part should only be done once, possibly in the startUp() method of your ingest module.
+
+\verbatim
+SleuthkitCase skCase = Case.getCurrentCaseThrows().getSleuthkitCase();
+
+// Add the new artifact type to the case if it does not already exist
+String artifactName = "APP_LOG";
+String artifactDisplayName = "Application Logins";
+BlackboardArtifact.Type artifactType = skCase.getArtifactType(artifactName);
+if (artifactType == null) {
+	artifactType = skCase.addBlackboardArtifactType(artifactName, artifactDisplayName);
+}
+
+// Add the new attribute type to the case if it does not already exist
+String attributeName = "LOGIN_DATA";
+String attributeDisplayName = "Login Data";
+BlackboardAttribute.Type loginDataAttributeType = skCase.getAttributeType(attributeName);
+if (loginDataAttributeType == null) {
+	loginDataAttributeType = skCase.addArtifactAttributeType(attributeName, 
+			BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON, attributeDisplayName);
+}
+\endverbatim
+
+You'll want to save the new artifact and attribute type objects to use later.
+
+Now our ingest module can create artifacts for the data it extracts. In the code below, we create our new "APP_LOG" artifact, add a standard attribute for the user name, and then create and store a JSON-formatted string which will contain each entry from the "loginData" list. Note that manually creating the JSON as shown below is not recommeded and is just for illustrative purposes - an easier method will be given afterward.
+
+\verbatim
+BlackboardArtifact art = content.newArtifact(artifactType.getTypeID());
+List<BlackboardAttribute> attributes = new ArrayList<>();
+attributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME, moduleName, appName));
+
+String jsonLoginStr = "{ LoginData : [ ";
+String dataStr = "";
+for(LoginData data : loginData) {
+	if (!dataStr.isEmpty()) {
+		dataStr += ", ";
+	}
+	dataStr += "{\"TSK_USER_NAME\" : \"" + data.getUserName() + "\", "
+			+ "\"TSK_DATATIME\" : \"" + data.getTimestamp() + "\"} ";
+	
+}
+jsonLoginStr += dataStr + " ] }";
+
+attributes.add(new BlackboardAttribute(loginDataAttributeType, moduleName, jsonLoginStr));
+art.addAttributes(attributes);
+\endverbatim
+
+It is important that each of the name-value pairs starts with an existing blackboard attribute name. This will allow Autopsy to use the corresponding value, for example, to extract out a timestamp to show this artifact in the <a href="http://sleuthkit.org/autopsy/docs/user-docs/latest/timeline_page.html">Timeline viewer</a>. Here's what our newly-created artifact will look like in Autopsy:
+
+\image html json_attribute.png
+
+The above method for storing the data works but formatting the JSON attribute manually is prone to errors. Luckily, in most cases instead of writing the JSON ourselves we can serialize a Java object. If the data that will go into the JSON attribute is contained in plain old Java objects (POJOs), then we can add annotations to that class to produce the JSON automatically. Here they've been added to the LoginData class:
+
+\verbatim
+// Requires package com.google.gson.annotations.SerializedName;
+private class LoginData {
+	@SerializedName("TSK_USER_NAME")
+	String userName;
+	
+	@SerializedName("TSK_DATETIME")
+	long timestamp;
+	
+	LoginData(String userName, long timestamp) {
+		this.userName = userName;
+		this.timestamp = timestamp;
+	}
+}
+\endverbatim
+
+We want our JSON attribute to store a list of these LoginData objects, so we'll create another POJO for that:
+
+\verbatim
+private class LoginDataLog {
+	List<LoginData> dataLog;
+	
+	LoginDataLog() {
+		dataLog = new ArrayList<>();
+	}
+	
+	void addData(LoginData data) {
+		dataLog.add(data);
+	}
+}
+\endverbatim
+
+Now we use org.sleuthkit.datamodel.blackboardutils.attributes.BlackboardJsonAttrUtil.toAttribute() to convert our LoginDataLog object into a BlackboardAttribute, greatly simplifying the code. Here, "dataLog" is an instance of a LoginDataLog object that contains all of the login data.
+
+\verbatim
+BlackboardArtifact art = content.newArtifact(artifactType.getTypeID());
+List<BlackboardAttribute> attributes = new ArrayList<>();
+attributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME, moduleName, appName));
+attributes.add(BlackboardJsonAttrUtil.toAttribute(loginDataAttributeType, moduleName, dataLog));
+art.addAttributes(attributes);
+\endverbatim
 
 
 */
diff --git a/bindings/java/doxygen/images/json_attribute.png b/bindings/java/doxygen/images/json_attribute.png
new file mode 100644
index 0000000000000000000000000000000000000000..3b25f869dd3ac34bf36870126a0f9fc6996047c3
Binary files /dev/null and b/bindings/java/doxygen/images/json_attribute.png differ
diff --git a/bindings/java/ivy.xml b/bindings/java/ivy.xml
index 14073535ff52e0c2de4bbcfe0a2cfa2707639c4e..af9f85d09f8d67a0bc96cab2f3647f211098b736 100644
--- a/bindings/java/ivy.xml
+++ b/bindings/java/ivy.xml
@@ -4,6 +4,7 @@
 		<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="commons-validator" name="commons-validator" rev="1.6"/>
 		
 		<dependency org="com.google.code.gson" name="gson" rev="2.8.5"/>
 		
diff --git a/bindings/java/nbproject/project.xml b/bindings/java/nbproject/project.xml
index ccbcee3fdd497578ef58a0350a783bc6615147c8..58ab53b9b262c0a5a2414f1bbbefc6a354627323 100755
--- a/bindings/java/nbproject/project.xml
+++ b/bindings/java/nbproject/project.xml
@@ -114,7 +114,7 @@
         <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;lib/joda-time-2.4.jar;lib/commons-lang3-3.0.jar;lib/guava-19.0.jar;lib/SparseBitSet-1.1.jar;lib/gson-2.8.5.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;lib/SparseBitSet-1.1.jar;lib/gson-2.8.5.jar;lib/commons-validator-1.6.jar</classpath>
                 <built-to>build</built-to>
                 <source-level>1.8</source-level>
             </compilation-unit>
diff --git a/bindings/java/src/org/sleuthkit/datamodel/BlackboardArtifact.java b/bindings/java/src/org/sleuthkit/datamodel/BlackboardArtifact.java
index 6197f4937ffc15ffa2902d02d9874087f2bcf057..b0c7f37f5de4cb3252324c32d186916b50021405 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/BlackboardArtifact.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/BlackboardArtifact.java
@@ -931,21 +931,29 @@ public enum ARTIFACT_TYPE implements SleuthkitVisitableItem {
 				bundle.getString("BlackboardArtifact.tskGenInfo.text")),
 		/**
 		 * A Web bookmark.
+		 * Use methods in org.sleuthkit.datamodel.blackboardutils.WebBrowserArtifactsHelper
+		 * to create bookmark artifacts.
 		 */
 		TSK_WEB_BOOKMARK(2, "TSK_WEB_BOOKMARK", //NON-NLS
 				bundle.getString("BlackboardArtifact.tskWebBookmark.text")),
 		/**
-		 * A Web cookie
+		 * A Web cookie.
+		 * Use methods in org.sleuthkit.datamodel.blackboardutils.WebBrowserArtifactsHelper
+		 * to create cookie artifacts.
 		 */
 		TSK_WEB_COOKIE(3, "TSK_WEB_COOKIE",
 				bundle.getString("BlackboardArtifact.tskWebCookie.text")), //NON-NLS				
 		/**
 		 * A Web history.
+		 * Use methods in org.sleuthkit.datamodel.blackboardutils.WebBrowserArtifactsHelper
+		 * to create history artifacts.
 		 */
 		TSK_WEB_HISTORY(4, "TSK_WEB_HISTORY", //NON-NLS
 				bundle.getString("BlackboardArtifact.tskWebHistory.text")),
 		/**
 		 * A Web download.
+		 * Use methods in org.sleuthkit.datamodel.blackboardutils.WebBrowserArtifactsHelper
+		 * to create download artifacts.
 		 */
 		TSK_WEB_DOWNLOAD(5, "TSK_WEB_DOWNLOAD", //NON-NLS
 				bundle.getString("BlackboardArtifact.tskWebDownload.text")),
@@ -1050,17 +1058,23 @@ public enum ARTIFACT_TYPE implements SleuthkitVisitableItem {
 		/**
 		 * A contact extracted from a phone, or from an address
 		 * book/email/messaging application.
+		 * Use methods in org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper
+		 * to create contact artifacts.
 		 */
 		TSK_CONTACT(23, "TSK_CONTACT", //NON-NLS
 				bundle.getString("BlackboardArtifact.tskContact.text")),
 		/**
 		 * An SMS/MMS message extracted from phone, or from another messaging
 		 * application, like IM.
+		 * Use methods in org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper
+		 * to create message artifacts.
 		 */
 		TSK_MESSAGE(24, "TSK_MESSAGE", //NON-NLS
 				bundle.getString("BlackboardArtifact.tskMessage.text")),
 		/**
 		 * A phone call log extracted from a phone or softphone application.
+		 * Use methods in org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper
+		 * to create call log artifacts.
 		 */
 		TSK_CALLLOG(25, "TSK_CALLLOG", //NON-NLS
 				bundle.getString("BlackboardArtifact.tskCalllog.text")),
@@ -1117,6 +1131,8 @@ public enum ARTIFACT_TYPE implements SleuthkitVisitableItem {
 				bundle.getString("BlackboardArtifact.tskInterestingArtifactHit.text")),
 		/**
 		 * A route based on GPS coordinates.
+		 * Use org.sleuthkit.datamodel.blackboardutils.GeoArtifactsHelper.addRoute()
+		 * to create route artifacts.
 		 */
 		TSK_GPS_ROUTE(36, "TSK_GPS_ROUTE", //NON-NLS
 				bundle.getString("BlackboardArtifact.tskGpsRoute.text")),
@@ -1181,12 +1197,16 @@ public enum ARTIFACT_TYPE implements SleuthkitVisitableItem {
 		TSK_DATA_SOURCE_USAGE(48, "TSK_DATA_SOURCE_USAGE", //NON-NLS
 				bundle.getString("BlackboardArtifact.tskDataSourceUsage.text")),
 		/**
-		 * Indicates auto fill data from a Web form
+		 * Indicates auto fill data from a Web form.
+		 * Use methods in org.sleuthkit.datamodel.blackboardutils.WebBrowserArtifactsHelper
+		 * to create web form autofill artifacts.
 		 */
 		TSK_WEB_FORM_AUTOFILL(49, "TSK_WEB_FORM_AUTOFILL", //NON-NLS
 				bundle.getString("BlackboardArtifact.tskWebFormAutofill.text")),
 		/**
-		 * Indicates an person's address filled in a web form
+		 * Indicates an person's address filled in a web form.
+		 * Use methods in org.sleuthkit.datamodel.blackboardutils.WebBrowserArtifactsHelper
+		 * to create web form address artifacts.
 		 */
 		TSK_WEB_FORM_ADDRESS(50, "TSK_WEB_FORM_ADDRESSES ", //NON-NLS
 				bundle.getString("BlackboardArtifact.tskWebFormAddresses.text")),
@@ -1229,9 +1249,19 @@ public enum ARTIFACT_TYPE implements SleuthkitVisitableItem {
 		 */
 		TSK_METADATA(57, "TSK_METADATA", //NON-NLS
 				bundle.getString("BlackboardArtifact.tskMetadata.text")),
-		
+		/**
+		 * Stores a GPS track log.
+		 * Use org.sleuthkit.datamodel.blackboardutils.GeoArtifactsHelper.addTrack() 
+		 * to create track artifacts.
+		 */
 		TSK_GPS_TRACK(58, "TSK_GPS_TRACK",
 				bundle.getString("BlackboardArtifact.tskTrack.text"));
+        /* To developers: For each new artifact, ensure that:
+         * - The enum value has 1-line JavaDoc description
+         * - The artifact catalog (artifact_catalog.dox) is updated to reflect the attributes it uses
+         */
+
+
 
 		private final String label;
 		private final int typeId;
diff --git a/bindings/java/src/org/sleuthkit/datamodel/BlackboardAttribute.java b/bindings/java/src/org/sleuthkit/datamodel/BlackboardAttribute.java
index 58c85b7ec844400ec87cac021a2fc4a3c71b11dd..6d64f87156a1ab934930d6bd3cc881506f0aaf65 100755
--- a/bindings/java/src/org/sleuthkit/datamodel/BlackboardAttribute.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/BlackboardAttribute.java
@@ -1369,18 +1369,42 @@ public enum ATTRIBUTE_TYPE {
 				bundle.getString("BlackboardAttribute.tskgroups.text"),
 				TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING),
 		
+		/*
+		 * Use org.sleuthkit.datamodel.blackboardutils.attributes.MessageAttachments to create and
+		 * process TSK_ATTACHMENTS attributes.
+		 */
 		TSK_ATTACHMENTS (141, "TSK_ATTACHMENTS", 
 				bundle.getString("BlackboardAttribute.tskattachments.text"),
 				TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON),
 		
+		/*
+		 * Use org.sleuthkit.datamodel.blackboardutils.attributes.GeoTrackPoints to create and
+		 * process TSK_GEO_TRACKPOINTS attributes.
+		 */
 		TSK_GEO_TRACKPOINTS(142, "TSK_GEO_TRACKPOINTS",
 			bundle.getString("BlackboardAttribute.tskgeopath.text"),
 			TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON),
 		
+		/*
+		 * Use org.sleuthkit.datamodel.blackboardutils.attributes.GeoWaypoints to create and
+		 * process TSK_GEO_WAYPOINTS attributes.
+		 */
 		TSK_GEO_WAYPOINTS(143, "TSK_GEO_WAYPOINTS",
 			bundle.getString("BlackboardAttribute.tskgeowaypoints.text"),
 			TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON),
 
+		TSK_DISTANCE_TRAVELED(144, "TSK_DISTANCE_TRAVELED",
+			bundle.getString("BlackboardAttribute.tskdistancetraveled.text"),
+			TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DOUBLE),
+		
+		TSK_DISTANCE_FROM_HOMEPOINT(145, "TSK_DISTANCE_FROM_HOMEPOINT",
+			bundle.getString("BlackboardAttribute.tskdistancefromhome.text"),
+			TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DOUBLE),
+		
+		TSK_HASH_PHOTODNA(146, "TSK_HASH_PHOTODNA",
+			bundle.getString("BlackboardAttribute.tskhashphotodna.text"),
+			TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING)
+		
 		;
 
 		private final int typeID;
diff --git a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties
index 96584aa91e722ee42eeaae568c339dc7c13f736c..c3728bb03f347f12a1e92bee77d64d6f1c2987da 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties
+++ b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties
@@ -193,6 +193,9 @@ BlackboardAttribute.tskgroups.text=Groups
 BlackboardAttribute.tskattachments.text=Message Attachments
 BlackboardAttribute.tskgeopath.text=List of Track Points
 BlackboardAttribute.tskgeowaypoints.text=List of Waypoints
+BlackboardAttribute.tskdistancetraveled.text=Distance Traveled
+BlackboardAttribute.tskdistancefromhome.text=Distance from Homepoint
+BlackboardAttribute.tskhashphotodna.text=PhotoDNA Hash
 AbstractFile.readLocal.exception.msg4.text=Error reading local file\: {0}
 AbstractFile.readLocal.exception.msg1.text=Error reading local file, local path is not set
 AbstractFile.readLocal.exception.msg2.text=Error reading local file, it does not exist at local path\: {0}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/CommunicationsManager.java b/bindings/java/src/org/sleuthkit/datamodel/CommunicationsManager.java
index e4bdea274df1d5b1d1ce04ec707a93c08d278523..8790b4bcaf3a3f29625489ba69ab318a2b3c1b6c 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/CommunicationsManager.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/CommunicationsManager.java
@@ -1315,52 +1315,19 @@ int getAccountTypeId(Account.Type accountType) {
 	 *
 	 * @return The normalized account id.
 	 */
-	private String normalizeAccountID(Account.Type accountType, String accountUniqueID) {
+	private String normalizeAccountID(Account.Type accountType, String accountUniqueID) throws TskCoreException {
 		String normailzeAccountID = accountUniqueID;
 
 		if (accountType.equals(Account.Type.PHONE)) {
-			normailzeAccountID = normalizePhoneNum(accountUniqueID);
+			normailzeAccountID = CommunicationsUtils.normalizePhoneNum(accountUniqueID);
 		} else if (accountType.equals(Account.Type.EMAIL)) {
-			normailzeAccountID = normalizeEmailAddress(accountUniqueID);
+			normailzeAccountID = CommunicationsUtils.normalizeEmailAddress(accountUniqueID);
 		}
 
 		return normailzeAccountID;
 	}
 
-	/**
-	 * Normalize the phone number by removing all non numeric characters, except
-	 * for leading +.
-	 *
-	 * @param phoneNum The phone number to normalize
-	 *
-	 * @return The normalized phone number.
-	 */
-	private String normalizePhoneNum(String phoneNum) {
-		String normailzedPhoneNum = phoneNum.replaceAll("\\D", "");
-
-		if (phoneNum.startsWith("+")) {
-			normailzedPhoneNum = "+" + normailzedPhoneNum;
-		}
-
-		if (normailzedPhoneNum.isEmpty()) {
-			normailzedPhoneNum = phoneNum;
-		}
-
-		return normailzedPhoneNum;
-	}
-
-	/**
-	 * Normalize the given email address by converting it to lowercase.
-	 *
-	 * @param emailAddress The email address tot normalize
-	 *
-	 * @return The normalized email address.
-	 */
-	private String normalizeEmailAddress(String emailAddress) {
-		String normailzedEmailAddr = emailAddress.toLowerCase();
-
-		return normailzedEmailAddr;
-	}
+	
 
 	/**
 	 * Builds the SQL for the given CommunicationsFilter.
diff --git a/bindings/java/src/org/sleuthkit/datamodel/CommunicationsUtils.java b/bindings/java/src/org/sleuthkit/datamodel/CommunicationsUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..0e603bef0efa88bc6751bea243b9da5616f3480f
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/CommunicationsUtils.java
@@ -0,0 +1,121 @@
+/*
+ * Sleuth Kit Data Model
+ *
+ * Copyright 2020 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 org.apache.commons.lang3.StringUtils;
+import org.apache.commons.validator.routines.EmailValidator;
+
+/**
+ * Provides general utility methods related to communications artifacts.
+ * 
+ */
+public final class CommunicationsUtils {
+	
+	/**
+     * Empty private constructor.
+     */
+    private CommunicationsUtils() {
+    }
+	
+	/**
+	 * Normalize the given phone number by removing all non numeric characters, 
+	 * except for a leading +.
+	 *
+	 * @param phoneNum The string to normalize.
+	 *
+	 * @return The normalized phone number.
+	 * 
+	 * @throws TskCoreException If the given string is not a valid phone number.
+	 * 
+	 */
+	public static String normalizePhoneNum(String phoneNum) throws TskCoreException {
+		if (isValidPhoneNumber(phoneNum)) {
+           return phoneNum.replaceAll("[^0-9\\+]", "");
+        } else {
+            throw new TskCoreException(String.format("Input string is not a valid phone number: %s", phoneNum));
+        }	
+	}
+
+	/**
+	 * Normalizes the given email address by converting it to lowercase.
+	 *
+	 * @param emailAddress The email address string to be normalized.
+	 *
+	 * @return The normalized email address.
+	 * @throws TskCoreException If the given string is not a valid email address.
+	 */
+	public static String normalizeEmailAddress(String emailAddress) throws TskCoreException {
+		
+        if (isValidEmailAddress(emailAddress)) {
+            return emailAddress.toLowerCase();
+        } else {
+            throw new TskCoreException(String.format("Input string is not a valid email address: %s", emailAddress));
+        }
+	}
+	
+	/**
+	 * Checks if the given accountId is a valid id for 
+	 * the specified account type.
+	 * 
+	 * @param accountType Account type.
+	 * @param accountUniqueID Id to check.
+	 * 
+	 * @return True, if the id is a valid id for the given account type, False otherwise.
+	 */
+	public static boolean isValidAccountId(Account.Type accountType, String accountUniqueID) {
+		if (accountType == Account.Type.PHONE) {
+			return isValidPhoneNumber(accountUniqueID);
+		}
+		if (accountType == Account.Type.EMAIL) {
+			return isValidPhoneNumber(accountUniqueID);
+		}
+		
+		return !StringUtils.isEmpty(accountUniqueID);
+	}
+	
+	/**
+	 * Checks if the given string is a valid phone number.
+	 *
+	 * @param phoneNum Phone number string to check.
+	 *
+	 * @return True if the given string is a valid phone number, false otherwise.
+	 */
+	public static boolean isValidPhoneNumber(String phoneNum) {
+		if (!StringUtils.isEmpty(phoneNum)) {
+			return phoneNum.matches("\\+?[0-9()\\-\\s]+");
+		}
+		return false;
+	}
+	
+	/**
+	 * Checks if the given string is a valid email address.
+	 *
+	 * @param emailAddress String to check.
+	 *
+	 * @return True if the given string is a valid email address, false otherwise.
+	 */
+	public static boolean isValidEmailAddress(String emailAddress) {
+		if (!StringUtils.isEmpty(emailAddress)) {
+			EmailValidator validator = EmailValidator.getInstance(true, true);
+			return validator.isValid(emailAddress);
+		}
+
+		return false;
+	}
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/DerivedFile.java b/bindings/java/src/org/sleuthkit/datamodel/DerivedFile.java
index 629a624323569cb4e6d28e29a2561db075752417..71833aeafe70f0cf1fd563d7e7e4bbe3dfbb1977 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/DerivedFile.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/DerivedFile.java
@@ -258,7 +258,6 @@ public String toString() {
 	}
 
 	/**
-	 * /**
 	 * Constructs a representation of a file or directory that has been derived
 	 * from another file and is stored outside of the data source (e.g., on a
 	 * user's machine). A typical example of a derived file is a file extracted
diff --git a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
index 20f36c5dde56bc396de0cd85163d3b19f1ef96bb..8d32c771006cd1a886d83ddea6590dd993210ec7 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
@@ -5936,15 +5936,15 @@ public Volume addVolume(long parentObjId, long addr, long start, long length, St
 	}
 	
 	/**
-	 * Add a pool to the database
-	 *
+	 * Add a pool to the database.
+	 * 
 	 * @param parentObjId Object ID of the pool's parent
-     * @param type        Type of pool
+	 * @param type        Type of pool
 	 * @param transaction Case DB transaction
-	 *
+	 * 
 	 * @return the newly created Pool
-	 *
-	 * @throws TskCoreException
+	 * 
+	 * @throws TskCoreException 
 	 */
 	public Pool addPool(long parentObjId, TskData.TSK_POOL_TYPE_ENUM type, CaseDbTransaction transaction) throws TskCoreException {
 		acquireSingleUserCaseWriteLock();
diff --git a/bindings/java/src/org/sleuthkit/datamodel/TimelineEventTypes.java b/bindings/java/src/org/sleuthkit/datamodel/TimelineEventTypes.java
index 1fb7cd76292126b94ace0eb621e79f904a04ded4..6444791cbf280a30ffa749c84a99f8b32148a0db 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/TimelineEventTypes.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/TimelineEventTypes.java
@@ -25,8 +25,8 @@
 import java.util.stream.Stream;
 import org.apache.commons.lang3.StringUtils;
 import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_TRACKPOINTS;
-import org.sleuthkit.datamodel.blackboardutils.attributes.TskGeoTrackpointsUtil;
-import org.sleuthkit.datamodel.blackboardutils.attributes.TskGeoTrackpointsUtil.GeoTrackPointList;
+import org.sleuthkit.datamodel.blackboardutils.attributes.BlackboardJsonAttrUtil;
+import org.sleuthkit.datamodel.blackboardutils.attributes.GeoTrackPoints;
 
 /**
  * Container class for various types of timeline events
@@ -124,9 +124,7 @@ TimelineEventDescription parseDescription(String fullDescriptionRaw, String medD
 	 * JSON list of waypoints from which a start time can be extracted.
 	 */
 	static class GPSTrackArtifactEventType extends TimelineEventArtifactTypeSingleDescription {
-		
-		private final TskGeoTrackpointsUtil trackpointUtil = new TskGeoTrackpointsUtil();
-		
+				
 		GPSTrackArtifactEventType(int typeID, String displayName, TimelineEventType superType, BlackboardArtifact.Type artifactType, BlackboardAttribute.Type descriptionAttribute) {
 			// Passing TSK_GEO_TRACKPOINTS as the "time attribute" as more of a place filler, to avoid any null issues
 			super(typeID, displayName, superType, artifactType, new BlackboardAttribute.Type(TSK_GEO_TRACKPOINTS), descriptionAttribute);
@@ -142,7 +140,12 @@ public TimelineEventDescriptionWithTime makeEventDescription(BlackboardArtifact
 			}
 			
 			// Get the waypoint list "start time"
-			GeoTrackPointList pointsList = trackpointUtil.fromAttribute(attribute);
+            GeoTrackPoints pointsList;
+			try {
+			pointsList = BlackboardJsonAttrUtil.fromAttribute(attribute, GeoTrackPoints.class);
+            } catch (BlackboardJsonAttrUtil.InvalidJsonException ex) {
+                throw new TskCoreException("Unable to parse track points in TSK_GEO_TRACKPOINTS attribute", ex);
+            }			
 			Long startTime = pointsList.getStartTime();
 			
 			// If we didn't find a startime do not create an event.
diff --git a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/ArtifactHelperBase.java b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/ArtifactHelperBase.java
index d8505ed5344aab1575f00baf5ef165f36cd5092e..426061af68d7aa82bbbd60bac035d7ab2eeda32e 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/ArtifactHelperBase.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/ArtifactHelperBase.java
@@ -1,7 +1,7 @@
 /*
  * Sleuth Kit Data Model
  *
- * Copyright 2019 Basis Technology Corp.
+ * Copyright 2019-2020 Basis Technology Corp.
  * Contact: carrier <at> sleuthkit <dot> org
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -75,11 +75,11 @@ String getModuleName() {
 	}
 
 	/**
-	 * Creates and adds an attribute of specified type to the given list, if the
+	 * Creates and adds a string attribute of specified type to the given list, if the
 	 * attribute value is not empty or null.
 	 *
 	 * @param attributeType Attribute type.
-	 * @param attrValue     Attribute value.
+	 * @param attrValue     String attribute value.
 	 * @param attributes    List of attributes to add to.
 	 *
 	 */
@@ -90,11 +90,11 @@ void addAttributeIfNotNull(BlackboardAttribute.ATTRIBUTE_TYPE attributeType, Str
 	}
 
 	/**
-	 * Creates and adds an attribute of specified type to the given list, if the
+	 * Creates and adds a long attribute of specified type to the given list, if the
 	 * attribute value is not 0.
 	 *
 	 * @param attributeType Attribute type.
-	 * @param attrValue     Attribute value.
+	 * @param attrValue     Long attribute value.
 	 * @param attributes    List of attributes to add to.
 	 */
 	void addAttributeIfNotZero(BlackboardAttribute.ATTRIBUTE_TYPE attributeType, long attrValue, Collection<BlackboardAttribute> attributes) {
@@ -102,4 +102,18 @@ void addAttributeIfNotZero(BlackboardAttribute.ATTRIBUTE_TYPE attributeType, lon
 			attributes.add(new BlackboardAttribute(attributeType, getModuleName(), attrValue));
 		}
 	}
+	
+	/**
+	 * Creates and adds an integer attribute of specified type to the given list, if the
+	 * attribute value is not 0.
+	 *
+	 * @param attributeType Attribute type.
+	 * @param attrValue     Integer attribute value.
+	 * @param attributes    List of attributes to add to.
+	 */
+	void addAttributeIfNotZero(BlackboardAttribute.ATTRIBUTE_TYPE attributeType, int attrValue, Collection<BlackboardAttribute> attributes) {
+		if (attrValue > 0) {
+			attributes.add(new BlackboardAttribute(attributeType, getModuleName(), attrValue));
+		}
+	}
 }
diff --git a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/CommunicationArtifactsHelper.java b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/CommunicationArtifactsHelper.java
index 0b8db6b5b4fa644776e5396b886846b21c83b799..e63275e0a348f3498c052f4cbeab0f204c387db1 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/CommunicationArtifactsHelper.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/CommunicationArtifactsHelper.java
@@ -33,6 +33,7 @@
 import org.sleuthkit.datamodel.Blackboard.BlackboardException;
 import org.sleuthkit.datamodel.BlackboardArtifact;
 import org.sleuthkit.datamodel.BlackboardAttribute;
+import org.sleuthkit.datamodel.CommunicationsUtils;
 import org.sleuthkit.datamodel.Content;
 import org.sleuthkit.datamodel.DataSource;
 import org.sleuthkit.datamodel.Relationship;
@@ -311,7 +312,7 @@ public BlackboardArtifact addContact(String contactName,
 
 	/**
 	 * Creates a contact's account instance of specified account type, if the
-	 * account id is not null/empty.
+	 * account id is not null/empty and is a valid account id for the account type.
 	 *
 	 * Also creates a CONTACT relationship between the self account and the new
 	 * contact account.
@@ -322,7 +323,7 @@ private void createContactMethodAccountAndRelationship(Account.Type accountType,
 
 		// Find/Create an account instance for each of the contact method
 		// Create a relationship between selfAccount and contactAccount
-		if (!StringUtils.isEmpty(accountUniqueID)) {
+		if (CommunicationsUtils.isValidAccountId(accountType, accountUniqueID)) {
 			AccountFileInstance contactAccountInstance = createAccountInstance(accountType, accountUniqueID);
 
 			// Create a relationship between self account and the contact account
@@ -506,7 +507,7 @@ public BlackboardArtifact addMessage(String messageType,
 
 		// set sender attribute and create sender account
 		AccountFileInstance senderAccountInstance = null;
-		if (!StringUtils.isEmpty(senderId)) {
+		if (CommunicationsUtils.isValidAccountId(moduleAccountsType, senderId)) {
 			senderAccountInstance = createAccountInstance(moduleAccountsType, senderId);
 		} 
 
@@ -515,7 +516,7 @@ public BlackboardArtifact addMessage(String messageType,
 		String recipientsStr = "";
 		if (!isEffectivelyEmpty(recipientIdsList)) {
 			for (String recipient : recipientIdsList) {
-				if (!StringUtils.isEmpty(recipient)) {
+				if (CommunicationsUtils.isValidAccountId(moduleAccountsType, recipient)) {
 					recipientAccountsList.add(createAccountInstance(moduleAccountsType, recipient));
 				}
 			}
@@ -551,16 +552,16 @@ public BlackboardArtifact addMessage(String messageType,
 				addAttributeIfNotNull(ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_TO, recipientsStr, attributes);
 				break;
 			default:  // direction UNKNOWN
-				
-				// if no sender, selfAccount substitutes caller.
 				if (StringUtils.isEmpty(senderId)) {
+					// if no sender, selfAccount substitutes caller.
 					senderAccountInstance = getSelfAccountInstance();
-				}	
-				// if no recipient specified, selfAccount substitutes recipient
-				if (isEffectivelyEmpty(recipientIdsList)) {
+				}
+				else if (isEffectivelyEmpty(recipientIdsList)) {
+					// else if no recipient specified, selfAccount substitutes recipient
 					recipientsStr = getSelfAccountInstance().getAccount().getTypeSpecificID();
 					recipientAccountsList.add(getSelfAccountInstance());
 				}	
+				
 				// save phone numbers in direction agnostic attributes
 				if (senderAccountInstance != null) {
 					addAttributeIfNotNull(ATTRIBUTE_TYPE.TSK_PHONE_NUMBER, senderAccountInstance.getAccount().getTypeSpecificID(), attributes);
@@ -741,7 +742,7 @@ public BlackboardArtifact addCalllog(CommunicationDirection direction,
 		addCommDirectionIfKnown(direction, attributes);
 
 		AccountFileInstance callerAccountInstance = null;
-		if (!StringUtils.isEmpty(callerId)) {
+		if (CommunicationsUtils.isValidAccountId(moduleAccountsType, callerId)) {
 			callerAccountInstance = createAccountInstance(moduleAccountsType, callerId);
 		}
 		
@@ -751,7 +752,7 @@ public BlackboardArtifact addCalllog(CommunicationDirection direction,
 		if (!isEffectivelyEmpty(calleeIdsList)) {
 			calleesStr = addressListToString(calleeIdsList);
 			for (String callee : calleeIdsList) {
-				if (!StringUtils.isEmpty(callee)) {
+				if (CommunicationsUtils.isValidAccountId(moduleAccountsType, callee)) {
 					recipientAccountsList.add(createAccountInstance(moduleAccountsType, callee));
 				}
 			}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/GeoArtifactsHelper.java b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/GeoArtifactsHelper.java
index 0e3f99f5a072d33678e470d91dfb72af8b68e6fe..558fda9b7f66d63e807050840ece29fa3bb63e84 100755
--- a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/GeoArtifactsHelper.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/GeoArtifactsHelper.java
@@ -19,7 +19,6 @@
 package org.sleuthkit.datamodel.blackboardutils;
 
 import java.util.ArrayList;
-import org.sleuthkit.datamodel.blackboardutils.attributes.TskGeoTrackpointsUtil.GeoTrackPointList;
 import java.util.List;
 import org.sleuthkit.datamodel.Blackboard.BlackboardException;
 import org.sleuthkit.datamodel.BlackboardArtifact;
@@ -27,64 +26,70 @@
 import org.sleuthkit.datamodel.Content;
 import org.sleuthkit.datamodel.SleuthkitCase;
 import org.sleuthkit.datamodel.TskCoreException;
-import org.sleuthkit.datamodel.blackboardutils.attributes.TskGeoWaypointsUtil.GeoWaypointList;
-import org.sleuthkit.datamodel.blackboardutils.attributes.TskGeoTrackpointsUtil;
-import org.sleuthkit.datamodel.blackboardutils.attributes.TskGeoWaypointsUtil;
+import org.sleuthkit.datamodel.blackboardutils.attributes.BlackboardJsonAttrUtil;
+import org.sleuthkit.datamodel.blackboardutils.attributes.GeoWaypoints;
+import org.sleuthkit.datamodel.blackboardutils.attributes.GeoTrackPoints;
 
 /**
- * Class to help ingest modules create Geolocation artifacts.
- *
+ * An artifact creation helper that adds geolocation artifacts to the case
+ * database.
  */
 public final class GeoArtifactsHelper extends ArtifactHelperBase {
 
+	private static final BlackboardAttribute.Type WAYPOINTS_ATTR_TYPE = new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_WAYPOINTS);
+	private static final BlackboardAttribute.Type TRACKPOINTS_ATTR_TYPE = new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_TRACKPOINTS);	
 	private final String programName;
-	private final TskGeoTrackpointsUtil trackPointAttributeUtil;
-	private final TskGeoWaypointsUtil waypointsAttributeUtil;
 
 	/**
-	 * Constructs a geolocation artifact helper for the given source file.
+	 * Constructs an artifact creation helper that adds geolocation artifacts to
+	 * the case database.
 	 *
-	 * @param caseDb		Sleuthkit case db.
-	 * @param moduleName	Name of module using the helper.
-	 * @param programName	Optional program name for TSK_PROG_NAME attribute, 
-	 *						nulls and empty string will be ignored.
-	 * @param srcFile		Source file being processed by the module.
+	 * @param caseDb      The case database.
+	 * @param moduleName  The name of the module creating the artifacts.
+	 * @param programName The name of the user application associated with the
+	 *                    geolocation data to be recorded as artifacts, may be
+	 *                    null. If a program name is supplied, it will be added
+	 *                    to each artifact that is created as a TSK_PROG_NAME
+	 *                    attribute.
+	 * @param srcContent  The source content for the artifacts, i.e., either a
+	 *                    file within a data source or a data source.
 	 */
-	public GeoArtifactsHelper(SleuthkitCase caseDb, String moduleName, String programName, Content srcFile) {
-		super(caseDb, moduleName, srcFile);
+	public GeoArtifactsHelper(SleuthkitCase caseDb, String moduleName, String programName, Content srcContent) {
+		super(caseDb, moduleName, srcContent);
 		this.programName = programName;
-		trackPointAttributeUtil = new TskGeoTrackpointsUtil();
-		waypointsAttributeUtil = new TskGeoWaypointsUtil();
 	}
 
 	/**
-	 * Add a Track from a GPS device to the database. A Track represents a 
-	 * series of points that the device has traveled on. This will create a 
-	 * TSK_GPS_TRACK artifact and add it to the case.
+	 * Adds a TSK_GPS_TRACK artifact to the case database. A Global Positioning
+	 * System (GPS) track artifact records the track, or path, of a GPS-enabled
+	 * device as a connected series of track points. A track point is a location
+	 * in a geographic coordinate system with latitude, longitude and altitude
+	 * (elevation) axes.
 	 *
-	 * @param trackName			Name of GPS track, not required.
-	 * @param points			List of GeoTrackPoints that the track traversed.
-	 *							Required.
-	 * @param moreAttributes	Optional list of other artifact attributes
+	 * @param trackName      The name of the GPS track, may be null.
+	 * @param trackPoints    The track points that make up the track.
+	 * @param moreAttributes Additional attributes for the TSK_GPS_TRACK
+	 *                       artifact, may be null.
 	 *
-	 * @return	TSK_GPS_TRACK artifact
+	 * @return	The TSK_GPS_TRACK artifact that was added to the case database.
 	 *
-	 * @throws TskCoreException		If there is an error creating the artifact.
-	 * @throws BlackboardException	If there is a problem posting the artifact
+	 * @throws TskCoreException	   If there is an error creating the artifact.
+	 * @throws BlackboardException If there is a error posting the artifact to
+	 *                             the blackboard.
 	 */
-	public BlackboardArtifact addTrack(String trackName, GeoTrackPointList points, List<BlackboardAttribute> moreAttributes) throws TskCoreException, BlackboardException {
-		
-		if(points == null) {
+	public BlackboardArtifact addTrack(String trackName, GeoTrackPoints trackPoints, List<BlackboardAttribute> moreAttributes) throws TskCoreException, BlackboardException {
+		if (trackPoints == null) {
 			throw new IllegalArgumentException(String.format("addTrack was passed a null list of track points"));
 		}
-		
+
 		BlackboardArtifact artifact = getContent().newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_TRACK);
 		List<BlackboardAttribute> attributes = new ArrayList<>();
+
 		if (trackName != null) {
 			attributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME, getModuleName(), trackName));
 		}
 
-		attributes.add(trackPointAttributeUtil.toAttribute(getModuleName(), points));
+		attributes.add(BlackboardJsonAttrUtil.toAttribute(TRACKPOINTS_ATTR_TYPE, getModuleName(), trackPoints));
 
 		if (programName != null) {
 			attributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME, getModuleName(), programName));
@@ -93,39 +98,45 @@ public BlackboardArtifact addTrack(String trackName, GeoTrackPointList points, L
 		if (moreAttributes != null) {
 			attributes.addAll(moreAttributes);
 		}
-		
+
 		artifact.addAttributes(attributes);
 
 		getSleuthkitCase().getBlackboard().postArtifact(artifact, getModuleName());
 
 		return artifact;
-	}
-
+	}	
+	
 	/**
-	 * Add a Route from a GPS device to the database. This will create a 
-	 * TSK_GPS_ROUTE artifact and add it to the case.
+	 * Adds a TSK_GPS_ROUTE artifact to the case database. A Global Positioning
+	 * System (GPS) route artifact records one or more waypoints entered into a
+	 * GPS-enabled device as a route to be navigated from waypoint to waypoint.
+	 * A waypoint is a location in a geographic coordinate system with latitude,
+	 * longitude and altitude (elevation) axes.
 	 *
-	 * @param routeName			Optional route name
-	 * @param creationTime		Time the route was created, optional.
-	 * @param points			List of GeoWaypointList belonging to the route, required
-	 * @param moreAttributes	Optional list of other artifact attributes.
+	 * @param routeName      The name of the GPS route, may be null.
+	 * @param creationTime   The time at which the route was created as
+	 *                       milliseconds from the Java epoch of
+	 *                       1970-01-01T00:00:00Z, may be null.
+	 * @param wayPoints      The waypoints that make up the route.
+	 * @param moreAttributes Additional attributes for the TSK_GPS_ROUTE
+	 *                       artifact, may be null.
 	 *
-	 * @return TSK_GPS_ROUTE artifact
+	 * @return	The TSK_GPS_ROUTE artifact that was added to the case database.
 	 *
-	 * @throws TskCoreException		If there is an error creating the artifact.
-	 * @throws BlackboardException	If there is a problem posting the artifact.
+	 * @throws TskCoreException	   If there is an error creating the artifact.
+	 * @throws BlackboardException If there is a error posting the artifact to
+	 *                             the blackboard.
 	 */
-	public BlackboardArtifact addRoute(String routeName, Long creationTime, GeoWaypointList points, List<BlackboardAttribute> moreAttributes) throws TskCoreException, BlackboardException {
-
-		if (points == null) {
+	public BlackboardArtifact addRoute(String routeName, Long creationTime, GeoWaypoints wayPoints, List<BlackboardAttribute> moreAttributes) throws TskCoreException, BlackboardException {
+		if (wayPoints == null) {
 			throw new IllegalArgumentException(String.format("addRoute was passed a null list of waypoints"));
 		}
 
 		BlackboardArtifact artifact = getContent().newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_ROUTE);
 		List<BlackboardAttribute> attributes = new ArrayList<>();
 
-		attributes.add(waypointsAttributeUtil.toAttribute(getModuleName(), points));
-		
+		attributes.add(BlackboardJsonAttrUtil.toAttribute(WAYPOINTS_ATTR_TYPE, getModuleName(), wayPoints));
+
 		if (routeName != null) {
 			attributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME, getModuleName(), routeName));
 		}
@@ -148,4 +159,5 @@ public BlackboardArtifact addRoute(String routeName, Long creationTime, GeoWaypo
 
 		return artifact;
 	}
+	
 }
diff --git a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/WebBrowserArtifactsHelper.java b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/WebBrowserArtifactsHelper.java
index 1a4cb5f2cb808720b6fcefaac49efa1a59fdbd2b..f3b7814666bf318c24a74dfe37b2650c7388a49a 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/WebBrowserArtifactsHelper.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/WebBrowserArtifactsHelper.java
@@ -30,6 +30,7 @@
 import org.sleuthkit.datamodel.BlackboardArtifact;
 import org.sleuthkit.datamodel.BlackboardAttribute;
 import org.sleuthkit.datamodel.CommunicationsManager;
+import org.sleuthkit.datamodel.CommunicationsUtils;
 import org.sleuthkit.datamodel.Content;
 import org.sleuthkit.datamodel.SleuthkitCase;
 import org.sleuthkit.datamodel.TskCoreException;
@@ -299,11 +300,11 @@ public BlackboardArtifact addWebFormAddress(String personName, String email,
 		Collection<BlackboardAttribute> attributes = new ArrayList<>();
 		
 		CommunicationsManager commManager = this.getSleuthkitCase().getCommunicationsManager();
-		if(StringUtils.isNotBlank(email)) {
+		if (CommunicationsUtils.isValidEmailAddress(email)) {
 			commManager.createAccountFileInstance(Account.Type.EMAIL, email, this.getModuleName(), this.getContent());
 		}
 
-		if(StringUtils.isNotBlank(phoneNumber)) {
+		if(CommunicationsUtils.isValidPhoneNumber(phoneNumber)) {
 			commManager.createAccountFileInstance(Account.Type.PHONE, phoneNumber, this.getModuleName(), this.getContent());
 		}
 
diff --git a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/BlackboardAttributeUtil.java b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/BlackboardAttributeUtil.java
deleted file mode 100755
index d32b264a56a6309498990d944c4125089b9bb48a..0000000000000000000000000000000000000000
--- a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/BlackboardAttributeUtil.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Sleuth Kit Data Model
- *
- * Copyright 2020 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.blackboardutils.attributes;
-
-import org.sleuthkit.datamodel.BlackboardAttribute;
-
-/**
- * An interface for Utility classes to implement for translating
- * BlackboardAttributes to and from a particular format. Initial use case is for
- * BlackboardAttributes of type TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON.
- */
-public interface BlackboardAttributeUtil<T> {
-
-	/**
-	 * Translates the value of type T to a attribute.
-	 *
-	 * @param moduleName	Name of module creating the artifact
-	 * @param value			Object to Translate to attribute
-	 *
-	 * @return BlackboardAttribute created from value
-	 */
-	BlackboardAttribute toAttribute(String moduleName, T value);
-
-	/**
-	 * Translates a attribute to an object of type T.
-	 *
-	 * @param attribute The attribute to be translated to T
-	 *
-	 * @return A new instance of T created from the attribute
-	 */
-	T fromAttribute(BlackboardAttribute attribute);
-}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/BlackboardJsonAttrUtil.java b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/BlackboardJsonAttrUtil.java
new file mode 100755
index 0000000000000000000000000000000000000000..fde3e728720ae559e6135a8cc2782434dc5a551f
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/BlackboardJsonAttrUtil.java
@@ -0,0 +1,119 @@
+/*
+ * Sleuth Kit Data Model
+ *
+ * Copyright 2020 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.blackboardutils.attributes;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import org.sleuthkit.datamodel.BlackboardAttribute;
+
+/**
+ * A utility for converting between JSON and artifact attributes of value type
+ * TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON.
+ */
+public final class BlackboardJsonAttrUtil {
+
+	/**
+	 * Creates an attribute of a given type with the string value set to an
+	 * object of type T serialized to JSON.
+	 *
+	 * T                 The type of the attribute value object to be
+	 *                   serialized.
+	 * 
+	 * @param attrType   The type of attribute to create.
+	 * @param moduleName The name of the module creating the attribute.
+	 * @param attrValue  The attribute value object.
+	 *
+	 * @return The BlackboardAttribute object.
+	 */
+	public static <T> BlackboardAttribute toAttribute(BlackboardAttribute.Type attrType, String moduleName, T attrValue) {
+		if (attrType.getValueType() != BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON) {
+			throw new IllegalArgumentException(String.format("Attribute type %s does not have value type BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON", attrType.getTypeName()));
+		}
+		return new BlackboardAttribute(attrType, moduleName, (new Gson()).toJson(attrValue));
+	}
+
+	/**
+	 * Creates an object of type T from the JSON in the string value of a
+	 * BlackboardAttribute with a value type of
+	 * TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON.
+	 *
+	 * T The type of the object to be created from the JSON.
+	 * 
+	 * @param attr  The attribute.
+	 * @param clazz The class object for class T.
+	 *
+	 * @return The T object from the attribute.
+	 *
+	 * @throws InvalidJsonException Thrown the JSON in an artifact attribute
+	 *                              cannot be deserialized to an object of the
+	 *                              specified type.
+	 */
+	public static <T> T fromAttribute(BlackboardAttribute attr, Class<T> clazz) throws InvalidJsonException {
+		BlackboardAttribute.Type attrType = attr.getAttributeType();
+		if (attrType.getValueType() != BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON) {
+			throw new IllegalArgumentException(String.format("Attribute type %s does not have value type BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON", attrType.getTypeName()));
+		}
+		String json = attr.getValueString();
+		if (json == null || json.isEmpty()) {
+			throw new InvalidJsonException("The string value (JSON) of the attribute is null or empty");
+		}
+		try {
+			return (new Gson()).fromJson(json, clazz);
+		} catch (JsonSyntaxException ex) {
+			throw new InvalidJsonException(String.format("The string value (JSON) could not be deserialized as a %s", clazz.getName()), ex);
+		}
+	}
+
+	/**
+	 * Constructs an exception to be thrown when the JSON in an artifact
+	 * attribute cannot be deserialized to an object of the specified type.
+	 */
+	public static class InvalidJsonException extends Exception {
+
+		private static final long serialVersionUID = 1L;
+
+		/**
+		 * Constructs an exception thrown when JSON in an artifact attribute
+		 * cannot be deserialized to an object of the specified type.
+		 *
+		 * @param message An error message.
+		 */
+		public InvalidJsonException(String message) {
+			super(message);
+		}
+
+		/**
+		 * Constructs an exception thrown when JSON in an artifact attribute
+		 * cannot be deserialized to an object of the specified type.
+		 *
+		 * @param message An error message.
+		 * @param cause   An excception that caused this exception to be thrown.
+		 */
+		public InvalidJsonException(String message, Throwable cause) {
+			super(message, cause);
+		}
+	}
+
+	/**
+	 * Prevents instantiation of this utility class.
+	 */
+	private BlackboardJsonAttrUtil() {
+	}
+
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/GeoTrackPoints.java b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/GeoTrackPoints.java
new file mode 100755
index 0000000000000000000000000000000000000000..fb61d5a53bd4234cf2d023a3e1a51fd0d20b807c
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/GeoTrackPoints.java
@@ -0,0 +1,232 @@
+/*
+ * Sleuth Kit Data Model
+ *
+ * Copyright 2020 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.blackboardutils.attributes;
+
+import com.google.gson.annotations.SerializedName;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * A GeoTrackPoints object is a collection of TrackPoint objects. A TrackPoint
+ * represents a track point, which is a location in a geographic coordinate
+ * system with latitude, longitude and altitude (elevation) axes.
+ *
+ * GeoTrackPoints objects are designed to be used as the string value of the
+ * TSK_GEO_TRACKPOINTS attribute of a TSK_GPS_TRACK artifact. TSK_GPS_TRACK
+ * artifacts are used to record a track, or path, of a GPS-enabled device as a
+ * connected series of track points.
+ *
+ */
+public class GeoTrackPoints implements Iterable<GeoTrackPoints.TrackPoint> {
+
+	private final List<TrackPoint> pointList;
+
+	/**
+	 * Constructs an empty GeoTrackPoints object.
+	 */
+	public GeoTrackPoints() {
+		pointList = new ArrayList<>();
+	}
+
+	/**
+	 * Adds a track point to this list of track points.
+	 *
+	 * @param trackPoint A track point.
+	 */
+	public void addPoint(TrackPoint trackPoint) {
+		if (trackPoint == null) {
+			throw new IllegalArgumentException("addPoint was passed a null track point");
+		}
+
+		pointList.add(trackPoint);
+	}
+
+	@Override
+	public Iterator<TrackPoint> iterator() {
+		return pointList.iterator();
+	}
+
+	/**
+	 * Returns whether or not this list of track points is empty.
+	 *
+	 * @return True or false.
+	 */
+	public boolean isEmpty() {
+		return pointList.isEmpty();
+	}
+
+	/**
+	 * Gets the earliest track point timestamp in this list of track points, if
+	 * timestamps are present.
+	 *
+	 * @return The timestamp in milliseconds from the Java epoch of
+	 *         1970-01-01T00:00:00Z, may be null or zero.
+	 */
+	public Long getStartTime() {
+		List<TrackPoint> orderedPoints = getTimeOrderedPoints();
+		if (orderedPoints != null) {
+			for (TrackPoint point : orderedPoints) {
+				if (point.getTimeStamp() != null) {
+					return point.getTimeStamp();
+				}
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * Gets the latest track point timestamp in this list of track points, if
+	 * timestamps are present.
+	 *
+	 * @return The timestamp in milliseconds from the Java epoch of
+	 *         1970-01-01T00:00:00Z, may be null or zero.
+	 */
+	public Long getEndTime() {
+		List<TrackPoint> orderedPoints = getTimeOrderedPoints();
+		if (orderedPoints != null) {
+			for (int index = orderedPoints.size() - 1; index >= 0; index--) {
+				TrackPoint point = orderedPoints.get(index);
+				if (point.getTimeStamp() != null) {
+					return point.getTimeStamp();
+				}
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * Gets this list of track points as a list ordered by track point
+	 * timestamp.
+	 *
+	 * @return The ordered list of track points.
+	 */
+	private List<TrackPoint> getTimeOrderedPoints() {
+		return pointList.stream().sorted().collect(Collectors.toCollection(ArrayList::new));
+	}
+
+	/**
+	 * A representation of a track point, which is a location in a geographic
+	 * coordinate system with latitude, longitude and altitude (elevation) axes.
+	 */
+	public final static class TrackPoint extends GeoWaypoints.Waypoint implements Comparable<TrackPoint> {
+
+		@SerializedName("TSK_GEO_VELOCITY")
+		private final Double velocity;
+		@SerializedName("TSK_DISTANCE_FROM_HOMEPOINT")
+		private final Double distanceFromHomePoint;
+		@SerializedName("TSK_DISTANCE_TRAVELED")
+		private final Double distanceTraveled;
+		@SerializedName("TSK_DATETIME")
+		private final Long timestamp;
+
+		/**
+		 * Constructs a representation of a track point, which is a location in
+		 * a geographic coordinate system with latitude, longitude and altitude
+		 * (elevation) axes.
+		 *
+		 * @param latitude              The latitude of the track point.
+		 * @param longitude             The longitude of the track point.
+		 * @param altitude              The altitude of the track point, may be
+		 *                              null.
+		 * @param name                  The name of the track point, may be
+		 *                              null.
+		 * @param velocity              The velocity of the device at the track
+		 *                              point in meters per second, may be null.
+		 * @param distanceFromHomePoint	The distance of the track point in
+		 *                              meters from an established home point,
+		 *                              may be null.
+		 * @param distanceTraveled      The distance the device has traveled in
+		 *                              meters at the time this track point was
+		 *                              created, may be null.
+		 * @param timestamp             The timestamp of the track point as
+		 *                              milliseconds from the Java epoch of
+		 *                              1970-01-01T00:00:00Z, may be null.
+		 */
+		public TrackPoint(Double latitude,
+				Double longitude,
+				Double altitude,
+				String name,
+				Double velocity,
+				Double distanceFromHomePoint,
+				Double distanceTraveled,
+				Long timestamp) {
+			super(latitude, longitude, altitude, name);
+			this.velocity = velocity;
+			this.distanceFromHomePoint = distanceFromHomePoint;
+			this.distanceTraveled = distanceTraveled;
+			this.timestamp = timestamp;
+		}
+
+		/**
+		 * Gets the velocity of the device at this track point in meters per
+		 * second, if known.
+		 *
+		 * @return The velocity in meters/sec, may be null or zero.
+		 */
+		public Double getVelocity() {
+			return velocity;
+		}
+
+		/**
+		 * Gets the distance of this track point from an established home point,
+		 * if known.
+		 *
+		 * @return The distance in meters, may be null or zero.
+		 */
+		public Double getDistanceFromHomePoint() {
+			return distanceFromHomePoint;
+		}
+
+		/**
+		 * Gets the distance the device has traveled in meters at the time this
+		 * track point was created, if known.
+		 *
+		 * @return The distance traveled in meters, may be null or zero.
+		 */
+		public Double getDistanceTraveled() {
+			return distanceTraveled;
+		}
+
+		/**
+		 * Gets the timestamp of this track point as milliseconds from the Java
+		 * epoch of 1970-01-01T00:00:00Z, if known.
+		 *
+		 * @return The timestamp, may be null or zero.
+		 */
+		public Long getTimeStamp() {
+			return timestamp;
+		}
+
+		@Override
+		public int compareTo(TrackPoint otherTP) {
+			Long otherTimeStamp = otherTP.getTimeStamp();
+
+			if (timestamp == null && otherTimeStamp != null) {
+				return -1;
+			} else if (timestamp != null && otherTimeStamp == null) {
+				return 1;
+			} else {
+				return timestamp.compareTo(otherTP.getTimeStamp());
+			}
+		}
+	}
+
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/GeoWaypoints.java b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/GeoWaypoints.java
new file mode 100755
index 0000000000000000000000000000000000000000..4e5f3ca192dd5816e26927e8b9b30951f5b1f412
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/GeoWaypoints.java
@@ -0,0 +1,153 @@
+/*
+ * Sleuth Kit Data Model
+ *
+ * Copyright 2020 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.blackboardutils.attributes;
+
+import com.google.gson.annotations.SerializedName;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * A GeoWaypoints object is a collection of Waypoint objects. A Waypoint object
+ * represents a waypoint for a GPS-enabled device with a navigation capability.
+ * Every waypoint is a location, possibly named, in a geographic coordinate
+ * system with latitude, longitude and altitude (elevation) axes.
+ *
+ * GeoWaypoints objects are designed to be used as the string value of the
+ * TSK_GEO_WAYPOINTS attribute of a TSK_GPS_ROUTE artifact. TSK_GPS_ROUTE
+ * artifacts are used to record one or more waypoints linked together as a route
+ * to be navigated from waypoint to waypoint.
+ */
+public class GeoWaypoints implements Iterable<GeoWaypoints.Waypoint> {
+
+	private final List<Waypoint> points;
+
+	/**
+	 * Constructs an empty GeoWaypoints object.
+	 */
+	public GeoWaypoints() {
+		points = new ArrayList<>();
+	}
+
+	/**
+	 * Adds a waypoint to this list of waypoints.
+	 *
+	 * @param wayPoint A waypoint.
+	 */
+	public void addPoint(Waypoint wayPoint) {
+		if (wayPoint == null) {
+			throw new IllegalArgumentException("addPoint was passed a null waypoint");
+		}
+
+		points.add(wayPoint);
+	}
+
+	/**
+	 * Returns whether or not this list of waypoints is empty.
+	 *
+	 * @return True or false.
+	 */
+	public boolean isEmpty() {
+		return points.isEmpty();
+	}
+
+	@Override
+	public Iterator<Waypoint> iterator() {
+		return points.iterator();
+	}
+
+	/**
+	 * A representation of a waypoint, which is a a location, possibly named, in
+	 * a geographic coordinate system with latitude, longitude and altitude
+	 * (elevation) axes.
+	 */
+	public static class Waypoint {
+
+		@SerializedName("TSK_GEO_LATITUDE")
+		private final Double latitude;
+		@SerializedName("TSK_GEO_LONGITUDE")
+		private final Double longitude;
+		@SerializedName("TSK_GEO_ALTITUDE")
+		private final Double altitude;
+		@SerializedName("TSK_NAME")
+		private final String name;
+
+		/**
+		 * Constructs a representation of a waypoint, which is a a location,
+		 * possibly named, in a geographic coordinate system with latitude,
+		 * longitude and altitude (elevation) axes.
+		 *
+		 * @param latitude  The latitude of the waypoint.
+		 * @param longitude The longitude of the waypoint.
+		 * @param altitude  The altitude of the waypoint, may be null.
+		 * @param name      The name of the waypoint, may be null.
+		 */
+		public Waypoint(Double latitude, Double longitude, Double altitude, String name) {
+			if (latitude == null) {
+				throw new IllegalArgumentException("Constructor was passed null latitude");
+			}
+
+			if (longitude == null) {
+				throw new IllegalArgumentException("Constructor was passed null longitude");
+			}
+
+			this.latitude = latitude;
+			this.longitude = longitude;
+			this.altitude = altitude;
+			this.name = name;
+		}
+
+		/**
+		 * Gets the latitude of this waypoint.
+		 *
+		 * @return The latitude.
+		 */
+		public Double getLatitude() {
+			return latitude;
+		}
+
+		/**
+		 * Gets the longitude of this waypoint.
+		 *
+		 * @return The longitude.
+		 */
+		public Double getLongitude() {
+			return longitude;
+		}
+
+		/**
+		 * Gets the altitude of this waypoint, if available.
+		 *
+		 * @return The altitude, may be null or zero.
+		 */
+		public Double getAltitude() {
+			return altitude;
+		}
+
+		/**
+		 * Gets the name of this waypoint, if available.
+		 *
+		 * @return	The name, may be null or empty.
+		 */
+		public String getName() {
+			return name;
+		}
+	}
+
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/MessageAttachments.java b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/MessageAttachments.java
index 632268d7409e2ee5073e6f66c4cdbae36c2216d1..c8b26442fb30c3141aa8ef231638de017c0bb309 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/MessageAttachments.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/MessageAttachments.java
@@ -333,7 +333,6 @@ public Long getObjId() {
 		}
 	}
 
-
 	private final Collection<FileAttachment> fileAttachments;
 	private final Collection<URLAttachment> urlAttachments;
 
diff --git a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/TskGeoTrackpointsUtil.java b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/TskGeoTrackpointsUtil.java
deleted file mode 100755
index ad434f5c7d3e1da86cbf7620629b9fb0a9c9b835..0000000000000000000000000000000000000000
--- a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/TskGeoTrackpointsUtil.java
+++ /dev/null
@@ -1,330 +0,0 @@
-/*
- * Sleuth Kit Data Model
- *
- * Copyright 2020 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.blackboardutils.attributes;
-
-import com.google.gson.Gson;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-import java.util.stream.Collectors;
-import org.sleuthkit.datamodel.BlackboardAttribute;
-import org.sleuthkit.datamodel.blackboardutils.attributes.TskGeoTrackpointsUtil.GeoTrackPointList;
-import org.sleuthkit.datamodel.blackboardutils.attributes.TskGeoTrackpointsUtil.GeoTrackPointList.GeoTrackPoint;
-
-/**
- * Utility class for translating TSK_GEO_TRACKPOINTS attribute values to
- * GeoTrackPointList objects and GeoTrackPointList to BlackboardAttributes.
- */
-public final class TskGeoTrackpointsUtil implements BlackboardAttributeUtil<GeoTrackPointList> {
-
-	@Override
-	public BlackboardAttribute toAttribute(String moduleName, GeoTrackPointList value) {
-
-		if (value == null) {
-			throw new IllegalArgumentException("toAttribute was passed a null list");
-		}
-
-		return new BlackboardAttribute(
-				BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_TRACKPOINTS,
-				moduleName,
-				toJSON(value));
-	}
-
-	@Override
-	public GeoTrackPointList fromAttribute(BlackboardAttribute attribute) {
-		if (attribute == null) {
-			throw new IllegalArgumentException("fromAttribute was passed a null attribute");
-		}
-
-		BlackboardAttribute.ATTRIBUTE_TYPE type = BlackboardAttribute.ATTRIBUTE_TYPE.fromID(attribute.getAttributeType().getTypeID());
-		if (type != BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_TRACKPOINTS) {
-			throw new IllegalArgumentException(String.format("Invalid attribute of type %s passed to fromAttribute method. Attribute of type TSK_GEO_TRACKPOINTS is required", type.getDisplayName()));
-		}
-
-		return fromJSON(attribute.getValueString());
-	}
-
-	/**
-	 * Creates a GeoTrackPointList from the given JSON string.
-	 *
-	 * @param jsonString JSon string of track points.
-	 *
-	 * @return	Timestamp ordered list of GeoTrackPoints, empty list will be
-	 *			returned if jsonString is null or empty.
-	 */
-	private static GeoTrackPointList fromJSON(String jsonString) {
-		if (jsonString == null || jsonString.isEmpty()) {
-			return null;
-		}
-
-		return (new Gson()).fromJson(jsonString, GeoTrackPointList.class);
-	}
-
-	/**
-	 * Returns a JSON string representing the given object.
-	 *
-	 * @return JSON string
-	 */
-	private static String toJSON(GeoTrackPointList pointList) {
-		Gson gson = new Gson();
-		return gson.toJson(pointList);
-	}
-
-	/**
-	 * A list of GeoTrackPoints.
-	 */
-	public static class GeoTrackPointList implements Iterable<GeoTrackPointList.GeoTrackPoint> {
-
-		private final List<GeoTrackPoint> pointList;
-
-		/**
-		 * Construct an empty GeoTrackPointList.
-		 */
-		public GeoTrackPointList() {
-			pointList = new ArrayList<>();
-		}
-
-		/**
-		 * Construct a new instance with the given list of GeoTrackPoint
-		 * objects.
-		 *
-		 * @param points List of track points, cannot be null.
-		 */
-		public GeoTrackPointList(List<GeoTrackPoint> points) {
-			if (points == null) {
-				throw new IllegalArgumentException("Constructor was passed a null list");
-			}
-
-			pointList = points;
-		}
-
-		/**
-		 * Add a point to the list of track points.
-		 *
-		 * @param point A point to add to the track point list, cannot be null.
-		 */
-		public void addPoint(GeoTrackPoint point) {
-			if (point == null) {
-				throw new IllegalArgumentException("addPoint was passed a null list");
-			}
-
-			pointList.add(point);
-		}
-
-		/**
-		 * Adds a new point with the given attributes.
-		 *
-		 * @param latitude			    Latitude of the trackpoint, required
-		 * @param longitude			    Longitude of the trackpoint, required
-		 * @param altitude			    Altitude of the trackpoint, maybe null
-		 * @param name				    Name of trackpoint, maybe null
-		 * @param velocity				Velocity in meters/sec, maybe null
-		 * @param distanceFromHomePoint	Track point distance from an established
-		 *                              "home point", may be null if not
-		 *                              applicable
-		 * @param distanceTraveled			   Overall distance traveled in meters at
-		 *                              the time this trackpoint was created,
-		 *                              maybe null if not applicable
-		 * @param timestamp					        Trackpoint creation time, maybe null if
-		 *                              not applicable
-		 */
-		public void addPoint(Double latitude,
-				Double longitude,
-				Double altitude,
-				String name,
-				Double velocity,
-				Double distanceFromHomePoint,
-				Double distanceTraveled,
-				Long timestamp) {
-			pointList.add(new GeoTrackPoint(
-					latitude,
-					longitude,
-					altitude,
-					name,
-					velocity,
-					distanceFromHomePoint,
-					distanceTraveled,
-					timestamp));
-		}
-
-		/**
-		 * Returns an iterator over the points in this GeoTrackPointList.
-		 *
-		 * @return An iterator over the elements of the list.
-		 */
-		@Override
-		public Iterator<GeoTrackPoint> iterator() {
-			return pointList.iterator();
-		}
-
-		/**
-		 * Returns true if this list contains no points.
-		 *
-		 * @return True if this list contains no points.
-		 */
-		public boolean isEmpty() {
-			return pointList.isEmpty();
-		}
-
-		/**
-		 * Return the start time for the track.
-		 *
-		 * @return First non-null time stamp or null, if one was not found.
-		 */
-		public Long getStartTime() {
-			List<GeoTrackPoint> orderedPoints = getTimeOrderedPoints();
-			if (orderedPoints != null) {
-				for (GeoTrackPoint point : orderedPoints) {
-					if (point.getTimeStamp() != null) {
-						return point.getTimeStamp();
-					}
-				}
-			}
-			return null;
-		}
-
-		/**
-		 * Return the ends time for the track.
-		 *
-		 * @return First non-null time stamp or null, if one was not found.
-		 */
-		public Long getEndTime() {
-			List<GeoTrackPoint> orderedPoints = getTimeOrderedPoints();
-			if (orderedPoints != null) {
-				for (int index = orderedPoints.size() - 1; index >= 0; index--) {
-					GeoTrackPoint point = orderedPoints.get(index);
-					if (point.getTimeStamp() != null) {
-						return point.getTimeStamp();
-					}
-				}
-			}
-			return null;
-		}
-
-		/**
-		 * Returns a timestamp ordered copy of the points list.
-		 *
-		 * @return List of points sorted by timestamps.
-		 */
-		private List<GeoTrackPoint> getTimeOrderedPoints() {
-			return pointList.stream().sorted().collect(Collectors.toCollection(ArrayList::new));
-		}
-
-		/**
-		 * A GeoTrackPoint is a Waypoint with more detailed information about
-		 * the point.
-		 *
-		 */
-		public final static class GeoTrackPoint extends TskGeoWaypointsUtil.GeoWaypointList.GeoWaypoint implements Comparable<GeoTrackPointList.GeoTrackPoint> {
-
-			private final Double velocity;
-			private final Double distanceFromHomePoint;
-			private final Double distanceTraveled;
-			private final Long timestamp;
-
-			/**
-			 * Constructs a GeoTrackPoint with the given attributes.
-			 *
-			 * @param latitude				Latitude of the track point, required
-			 * @param longitude				Longitude of the track point,
-			 *                              required
-			 * @param altitude			    Altitude of the track point, may be
-			 *                              null
-			 * @param name				    Name of track point, may be null
-			 * @param velocity			    Velocity in meters/sec, may be null
-			 * @param distanceFromHomePoint	Track point distance from an
-			 *                              established "home point", maybe null
-			 *                              if not applicable
-			 * @param distanceTraveled	    Overall distance traveled in meters
-			 *                              at the time this track point was
-			 *                              created, maybe null if not
-			 *                              applicable
-			 * @param timestamp			    Track point creation time, maybe
-			 *                              null if not applicable
-			 */
-			public GeoTrackPoint(Double latitude,
-					Double longitude,
-					Double altitude,
-					String name,
-					Double velocity,
-					Double distanceFromHomePoint,
-					Double distanceTraveled,
-					Long timestamp) {
-				super(latitude, longitude, altitude, name);
-				this.velocity = velocity;
-				this.distanceFromHomePoint = distanceFromHomePoint;
-				this.distanceTraveled = distanceTraveled;
-				this.timestamp = timestamp;
-			}
-
-			/**
-			 * Returns velocity of the point.
-			 *
-			 * @return Double velocity value, maybe null if not available or
-			 *         applicable
-			 */
-			public Double getVelocity() {
-				return velocity;
-			}
-
-			/**
-			 * Returns distance from home point for the point.
-			 *
-			 * @return Double velocity distance from home point, maybe null if
-			 *         not available or applicable
-			 */
-			public Double getDistanceFromHomePoint() {
-				return distanceFromHomePoint;
-			}
-
-			/**
-			 * Returns distance traveled for the point.
-			 *
-			 * @return Double distance traveled value, maybe null if not
-			 *         available or applicable
-			 */
-			public Double getDistanceTraveled() {
-				return distanceTraveled;
-			}
-
-			/**
-			 * Returns the time stamp (seconds from java/unix epoch) of the
-			 * track point.
-			 *
-			 * @return time stamp of the track point, or null if not available
-			 */
-			public Long getTimeStamp() {
-				return timestamp;
-			}
-
-			@Override
-			public int compareTo(GeoTrackPointList.GeoTrackPoint otherTP) {
-				Long otherTimeStamp = otherTP.getTimeStamp();
-
-				if (timestamp == null && otherTimeStamp != null) {
-					return -1;
-				} else if (timestamp != null && otherTimeStamp == null) {
-					return 1;
-				} else {
-					return timestamp.compareTo(otherTP.getTimeStamp());
-				}
-			}
-		}
-	}
-}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/TskGeoWaypointsUtil.java b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/TskGeoWaypointsUtil.java
deleted file mode 100755
index 08d5a1c6bd6a77ab67bf0992c5a73a2d4e58ebe5..0000000000000000000000000000000000000000
--- a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/attributes/TskGeoWaypointsUtil.java
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * Sleuth Kit Data Model
- *
- * Copyright 2020 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.blackboardutils.attributes;
-
-import com.google.gson.Gson;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-import org.sleuthkit.datamodel.BlackboardAttribute;
-import org.sleuthkit.datamodel.blackboardutils.attributes.TskGeoWaypointsUtil.GeoWaypointList;
-import org.sleuthkit.datamodel.blackboardutils.attributes.TskGeoWaypointsUtil.GeoWaypointList.GeoWaypoint;
-
-/**
- * Utility class for Translating TSK_GEO_WAYPOINTS attribute values to
- * GeoWaypointList objects and GeoWaypointList to BlackboardAttributes.
- */
-public final class TskGeoWaypointsUtil implements BlackboardAttributeUtil<GeoWaypointList> {
-
-	@Override
-	public BlackboardAttribute toAttribute(String moduleName, GeoWaypointList value) {
-
-		if (value == null) {
-			throw new IllegalArgumentException("toAttribute was pass a null list");
-		}
-
-		return new BlackboardAttribute(
-				BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_WAYPOINTS,
-				moduleName,
-				toJSON(value));
-	}
-
-	@Override
-	public GeoWaypointList fromAttribute(BlackboardAttribute attribute) {
-		if (attribute == null) {
-			throw new IllegalArgumentException("fromAttribute was pass a null list");
-		}
-
-		BlackboardAttribute.ATTRIBUTE_TYPE type = BlackboardAttribute.ATTRIBUTE_TYPE.fromID(attribute.getAttributeType().getTypeID());
-		if (type != BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_WAYPOINTS) {
-			throw new IllegalArgumentException(String.format("Invalid attribute of type %s passed to fromAttribute method. Attribute of type TSK_GEO_WAYPOINTS is required", type.getDisplayName()));
-		}
-
-		return fromJSON(attribute.getValueString());
-	}
-
-	/**
-	 * Deserialize the given list of GeoTrackPoints.
-	 *
-	 * @param jsonString JSon string of track points.
-	 *
-	 * @return	Timestamp ordered list of GeoTrackPoints, empty list will be
-	 *         returned if jsonString is null or empty.
-	 */
-	private static GeoWaypointList fromJSON(String jsonString) {
-		if (jsonString == null || jsonString.isEmpty()) {
-			return null;
-		}
-
-		return (new Gson()).fromJson(jsonString, GeoWaypointList.class);
-	}
-
-	/**
-	 * Returns a JSON string can than be used as the TSK_GEO_TRACKPOINTS
-	 * attribute of the TSK_GPS_TRACK artifact.
-	 *
-	 * @return JSON string
-	 */
-	private static String toJSON(GeoWaypointList pointList) {
-		Gson gson = new Gson();
-		return gson.toJson(pointList);
-	}
-
-	/**
-	 * Helper class to make it easier to serialize and deserialize the list of
-	 * waypoints points with json.
-	 *
-	 */
-	public static final class GeoWaypointList implements Iterable<GeoWaypointList.GeoWaypoint> {
-
-		private final List<GeoWaypoint> points;
-
-		public GeoWaypointList() {
-			points = new ArrayList<>();
-		}
-
-		/**
-		 * Adds a point to the list of waypoints.
-		 *
-		 * @param latitude  The latitude, required
-		 * @param longitude The longitude, required
-		 * @param altitude  The altitude, can be null
-		 * @param name		A name for the point, can be null
-		 */
-		public void addPoint(Double latitude, Double longitude, Double altitude, String name) {
-			points.add(new GeoWaypoint(latitude, longitude, altitude, name));
-		}
-
-		/**
-		 * Returns true if this list contains no points.
-		 *
-		 * @return True if this list contains no points.
-		 */
-		public boolean isEmpty() {
-			return points.isEmpty();
-		}
-
-		@Override
-		public Iterator<GeoWaypointList.GeoWaypoint> iterator() {
-			return points.iterator();
-		}
-
-		/**
-		 * Class that represents a single waypoint made up of longitude,
-		 * latitude, and altitude.
-		 */
-		public static class GeoWaypoint {
-
-			private final Double latitude;
-			private final Double longitude;
-			private final Double altitude;
-			private final String name;
-
-			/**
-			 * Creates a GeoWaypoint instance.
-			 *
-			 * @param latitude  The latitude, required
-			 * @param longitude The longitude, required
-			 * @param altitude  The altitude, can be null
-			 * @param name		A name for the waypoint, optional
-			 */
-			public GeoWaypoint(Double latitude, Double longitude, Double altitude, String name) {
-				if (latitude == null || longitude == null) {
-					throw new IllegalArgumentException("Constructor was passed null coordinate");
-				}
-
-				this.latitude = latitude;
-				this.longitude = longitude;
-				this.altitude = altitude;
-				this.name = name;
-			}
-
-			/**
-			 * Returns latitude of the waypoint.
-			 *
-			 * @return Double latitude value
-			 */
-			public Double getLatitude() {
-				return latitude;
-			}
-
-			/**
-			 * Returns longitude of the waypoint.
-			 *
-			 * @return Double longitude value
-			 */
-			public Double getLongitude() {
-				return longitude;
-			}
-
-			/**
-			 * Get the altitude if available for this waypoint.
-			 *
-			 * @return Double altitude value, may be null if not available or
-			 *         applicable
-			 */
-			public Double getAltitude() {
-				return altitude;
-			}
-
-			/**
-			 * Returns the name for this waypoint.
-			 *
-			 * @return	Returns waypoint name, may be null if not available or
-			 *         applicable.
-			 */
-			public String getName() {
-				return name;
-			}
-		}
-	}
-}
diff --git a/configure.ac b/configure.ac
index 47aa7f5c8491929bdee35c45e8a8d12e60f1dd69..7d9e6b23159a00d5f39a28d37d0fd6760453e59e 100644
--- a/configure.ac
+++ b/configure.ac
@@ -4,7 +4,7 @@ dnl Process this file with autoconf to produce a configure script.
 
 AC_PREREQ(2.59)
 
-AC_INIT(sleuthkit, 4.8.0)
+AC_INIT(sleuthkit, 4.9.0)
 m4_include([m4/ax_pthread.m4])
 dnl include the version from 1.12.1. This will work for
 m4_include([m4/cppunit.m4])
@@ -388,7 +388,7 @@ AC_CHECK_FUNCS([getline])
 AC_SEARCH_LIBS(regexec, [regex], , AC_MSG_ERROR([missing regex]))
 
 
-dnl OpenSSL support for encryption
+dnl OpenSSL support for encryption - currently disabled due to automatic test failures
 dnl AX_CHECK_OPENSSL(
 dnl  [ax_openssl=yes]
 dnl  LIBTSK_LDFLAGS="$LIBTSK_LDFLAGS $OPENSSL_LDFLAGS $OPENSSL_LIBS",
@@ -441,12 +441,12 @@ AC_CONFIG_FILES([
 AC_OUTPUT
 
 dnl Print a summary
+dnl openssl is disabled, so removed line openssl support: $ax_openssl
 AC_MSG_NOTICE([
 Building:
    afflib support:                        $ax_afflib
    libewf support:                        $ax_libewf
    zlib support:                          $ax_zlib
-   openssl support:                       $ax_openssl
 
    libvhdi support:                       $ax_libvhdi
    libvmdk support:                       $ax_libvmdk
diff --git a/debian/changelog b/debian/changelog
index 768f923d4c9a6a7d29410e1b979db1fb7c4b83bb..1690c7fd522c0cf459a15909a66e8f8ce6cfb6e9 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,4 +1,4 @@
-sleuthkit-java (4.8.0-1) unstable; urgency=medium
+sleuthkit-java (4.9.0-1) unstable; urgency=medium
 
   * Initial release (Closes: #nnnn)  <nnnn is the bug number of your ITP>
 
diff --git a/debian/sleuthkit-java.install b/debian/sleuthkit-java.install
index 9fc3bd7bbb9cfeb20fde972ee23f9bb1d68049e4..ac1d072cac56c7c660e5be1cfe77d0d79963a707 100644
--- a/debian/sleuthkit-java.install
+++ b/debian/sleuthkit-java.install
@@ -1,3 +1,3 @@
 bindings/java/lib/sqlite-jdbc-3.25.2.jar /usr/share/java
-bindings/java/dist/sleuthkit-4.8.0.jar /usr/share/java
+bindings/java/dist/sleuthkit-4.9.0.jar /usr/share/java
 
diff --git a/licenses/bsd.txt b/licenses/bsd.txt
new file mode 100644
index 0000000000000000000000000000000000000000..ffb82a69aa34be508b5cba51e4b2219b267e43f2
--- /dev/null
+++ b/licenses/bsd.txt
@@ -0,0 +1,16 @@
+The following files use the 3-Clause BSD license:
+
+tsk/fs/lzvn.c
+Copyright (c) 2015-2016, Apple Inc. All rights reserved.
+
+
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:  
+
+1.  Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+2.  Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+3.  Neither the name of the copyright holder(s) nor the names of any contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/licenses/mit.txt b/licenses/mit.txt
new file mode 100644
index 0000000000000000000000000000000000000000..527c48ea76b56af6f87b7007164864316ec1d885
--- /dev/null
+++ b/licenses/mit.txt
@@ -0,0 +1,24 @@
+The following files use the MIT license:
+
+tsk/auto/guid.cpp
+Copyright (c) 2014 Graeme Hill (http://graemehill.ca)
+
+
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
\ No newline at end of file
diff --git a/packages/sleuthkit.spec b/packages/sleuthkit.spec
index e4d5b16e5b7529b4c7798e00d48a470e55280e12..4439db52aad80e20aa0f3b0238a2658e46d73294 100644
--- a/packages/sleuthkit.spec
+++ b/packages/sleuthkit.spec
@@ -1,5 +1,5 @@
 Name:		sleuthkit	
-Version:	4.8.0
+Version:	4.9.0
 Release:	1%{?dist}
 Summary:	The Sleuth Kit (TSK) is a library and collection of command line tools that allow you to investigate volume and file system data.	
 
diff --git a/tsk/Makefile.am b/tsk/Makefile.am
index d66d3126cc87f4f5ed2bd2caaa7735099552e5b7..a17d3de69df534508d971bf8548d96bb65ef9ede 100644
--- a/tsk/Makefile.am
+++ b/tsk/Makefile.am
@@ -8,6 +8,6 @@ libtsk_la_LIBADD = base/libtskbase.la img/libtskimg.la \
     vs/libtskvs.la fs/libtskfs.la hashdb/libtskhashdb.la \
     auto/libtskauto.la pool/libtskpool.la util/libtskutil.la
 # current:revision:age
-libtsk_la_LDFLAGS = -version-info 20:0:1 $(LIBTSK_LDFLAGS)
+libtsk_la_LDFLAGS = -version-info 20:1:1 $(LIBTSK_LDFLAGS)
 
 EXTRA_DIST = tsk_tools_i.h docs/Doxyfile docs/*.dox docs/*.html
diff --git a/tsk/auto/auto.cpp b/tsk/auto/auto.cpp
index fc95317e234d14153a54749a07718e83ba59eff9..e20b1dac8b4520117cb88a7ab7c784c684fa07f2 100755
--- a/tsk/auto/auto.cpp
+++ b/tsk/auto/auto.cpp
@@ -149,6 +149,7 @@ void
     for (int i = 0; i < m_poolInfos.size(); i++) {
         tsk_pool_close(m_poolInfos[i]);
     }
+    m_poolInfos.clear();
 
     if ((m_img_info) && (m_internalOpen)) {
         tsk_img_close(m_img_info);
diff --git a/tsk/base/tsk_base.h b/tsk/base/tsk_base.h
index 47b1e50dc1875eabda0c42c42a7a26c17d6b48b1..91daa425392cd9526b9d9ca770d69b908d42c267 100644
--- a/tsk/base/tsk_base.h
+++ b/tsk/base/tsk_base.h
@@ -39,11 +39,11 @@
  * 3.1.2b1 would be 0x03010201.  Snapshot from Jan 2, 2003 would be
  * 0xFF030102.
  * See TSK_VERSION_STR for string form. */
-#define TSK_VERSION_NUM 0x040800ff
+#define TSK_VERSION_NUM 0x040900ff
 
 /** Version of code in string form. See TSK_VERSION_NUM for
  * integer form. */
-#define TSK_VERSION_STR "4.8.0"
+#define TSK_VERSION_STR "4.9.0"
 
 
 /* include the TSK-specific header file that we created in autoconf
diff --git a/tsk/docs/Doxyfile b/tsk/docs/Doxyfile
index 2286063987879542a0d9a94babacab2707f84039..cf6380f2c8e7cd4552e12a4e7360b5b61027b26f 100644
--- a/tsk/docs/Doxyfile
+++ b/tsk/docs/Doxyfile
@@ -33,7 +33,7 @@ PROJECT_NAME           = "The Sleuth Kit"
 # if some version control system is used.
 
 # This is automatically updated  at release time. 
-PROJECT_NUMBER         = 4.8.0
+PROJECT_NUMBER = 4.9.0
 
 # Using the PROJECT_BRIEF tag one can provide an optional one line description
 # for a project that appears at the top of each page and should give viewer
@@ -883,7 +883,7 @@ GENERATE_HTML          = YES
 # put in front of it. If left blank `html' will be used as the default path.
 
 # NOTE: This is automatically updated at release time. 
-HTML_OUTPUT            = api-docs/4.8.0/
+HTML_OUTPUT = api-docs/4.9.0/
 
 # The HTML_FILE_EXTENSION tag can be used to specify the file extension for
 # each generated HTML page (for example: .htm,.php,.asp). If it is left blank
diff --git a/tsk/docs/hashdb.dox b/tsk/docs/hashdb.dox
index b28400f5f16c701da79659e29d558259a107f5c8..0d7166979536c68ef94b9f63162c4d1f42511aae 100644
--- a/tsk/docs/hashdb.dox
+++ b/tsk/docs/hashdb.dox
@@ -4,7 +4,7 @@ This section describes some the API functions and concepts associated with the H
 
 \section hash_over Overview
 
-Hash databases are frequently used to identify known good and known bad files. Text files of MD5 and SHA-1 hashes can be easily created and shared, but they are frequently not the most efficient to use to use when searching for a hash because they are in an unsorted order. 
+Hash databases are frequently used to identify known good and known bad files. Text files of MD5 and SHA-1 hashes can be easily created and shared, but they are frequently not the most efficient to use when searching for a hash because they are in an unsorted order. 
 
 The hash database functions in TSK create an index into text file hash databases and allow you to more quickly perform lookups.  TSK  uses the index to perform binary searches for the hashes (see <a href="http://www.sleuthkit.org/informer/sleuthkit-informer-6.html">Informer #6</a>). 
 
diff --git a/tsk/fs/fs_types.c b/tsk/fs/fs_types.c
index 6ba88f0e6f1e1065fbdedb93fe4b512c4155f334..eef6b4fae4c43afbcf978a6198b49586991a54f8 100644
--- a/tsk/fs/fs_types.c
+++ b/tsk/fs/fs_types.c
@@ -39,16 +39,18 @@ typedef struct {
  * name so that we can map between values and names. 
  */
 static FS_TYPES fs_type_table[] = {
-    {"ntfs", TSK_FS_TYPE_NTFS_DETECT, "NTFS"},
+    {"ntfs", TSK_FS_TYPE_NTFS, "NTFS"}, // NTFS == NTFS_DETECT 
     {"fat", TSK_FS_TYPE_FAT_DETECT, "FAT (Auto Detection)"},
     {"ext", TSK_FS_TYPE_EXT_DETECT, "ExtX (Auto Detection)"},
-    {"iso9660", TSK_FS_TYPE_ISO9660_DETECT, "ISO9660 CD"},
+    {"iso9660", TSK_FS_TYPE_ISO9660, "ISO9660 CD"}, // ISO9660 == DETECT
 #if TSK_USE_HFS
-    {"hfs", TSK_FS_TYPE_HFS_DETECT, "HFS+"},
+    {"hfs", TSK_FS_TYPE_HFS_DETECT, "HFS+ (Auto Detection)"},
 #endif
+    {"yaffs2", TSK_FS_TYPE_YAFFS2, "YAFFS2"},
+    {"apfs", TSK_FS_TYPE_APFS, "APFS"},
     {"ufs", TSK_FS_TYPE_FFS_DETECT, "UFS (Auto Detection)"},
-    {"raw", TSK_FS_TYPE_RAW_DETECT, "Raw Data"},
-    {"swap", TSK_FS_TYPE_SWAP_DETECT, "Swap Space"},
+    {"raw", TSK_FS_TYPE_RAW, "Raw Data"}, // RAW == RAW_DETECT
+    {"swap", TSK_FS_TYPE_SWAP, "Swap Space"}, // SWAP == SWAP_DETECT
     {"fat12", TSK_FS_TYPE_FAT12, "FAT12"},
     {"fat16", TSK_FS_TYPE_FAT16, "FAT16"},
     {"fat32", TSK_FS_TYPE_FAT32, "FAT32"},
@@ -58,8 +60,10 @@ static FS_TYPES fs_type_table[] = {
     {"ext4", TSK_FS_TYPE_EXT4, "Ext4"},
     {"ufs1", TSK_FS_TYPE_FFS1, "UFS1"},
     {"ufs2", TSK_FS_TYPE_FFS2, "UFS2"},
-    {"yaffs2", TSK_FS_TYPE_YAFFS2, "YAFFS2"},
-    {"apfs", TSK_FS_TYPE_APFS, "APFS"},
+#if TSK_USE_HFS
+    {"hfsp", TSK_FS_TYPE_HFS, "HFS+"},
+    {"hfsl", TSK_FS_TYPE_HFS_LEGACY, "HFS (Legacy)"},
+#endif
     {0,0,""},
 };
 
diff --git a/tsk/fs/ntfs.c b/tsk/fs/ntfs.c
index eeff809ef191244e4bb974dcbe9793ca518888ce..c55ee59f71eb1a565c49c8f8f7d4e0b7ca1a4696 100755
--- a/tsk/fs/ntfs.c
+++ b/tsk/fs/ntfs.c
@@ -375,7 +375,9 @@ ntfs_dinode_lookup(NTFS_INFO * a_ntfs, char *a_buf, TSK_INUM_T a_mftnum)
             ("dinode_lookup: More Update Sequence Entries than MFT size");
         return TSK_COR;
     }
-    if (tsk_getu16(fs->endian, mft->upd_off) + sizeof(ntfs_upd) > a_ntfs->mft_rsize_b) {
+    if (tsk_getu16(fs->endian, mft->upd_off) + 
+            sizeof(ntfs_upd) + 
+            2*(tsk_getu16(fs->endian, mft->upd_cnt) - 1) > a_ntfs->mft_rsize_b) {
         tsk_error_reset();
         tsk_error_set_errno(TSK_ERR_FS_INODE_COR);
         tsk_error_set_errstr
diff --git a/tsk/img/raw.c b/tsk/img/raw.c
index 861e1ff8db7334336c306e60e40aab344fb1b2d0..daf19babeb0bc585a65f564ddbcc37bb304dec6a 100755
--- a/tsk/img/raw.c
+++ b/tsk/img/raw.c
@@ -39,6 +39,17 @@
 #define S_IFDIR __S_IFDIR
 #endif
 
+/**
+ * \internal
+ * Test if the image is a Windows device
+ * @param The path to test
+ *
+ * Return 1 if the path represents a Windows device, 0 otherwise
+ */
+static int
+is_windows_device_path(const TSK_TCHAR * image_name) {
+    return (TSTRNCMP(image_name, _TSK_T("\\\\.\\"), 4) == 0);
+}
 
 /** 
  * \internal
@@ -133,7 +144,7 @@ raw_read_segment(IMG_RAW_INFO * raw_info, int idx, char *buf,
         // If the offset to seek to isn't sector-aligned and this is a device, we need to start at the previous sector boundary and
         // read some extra data.
         if ((offset_to_read % raw_info->img_info.sector_size != 0)
-                && (TSTRNCMP(raw_info->img_info.images[idx], _TSK_T("\\\\.\\"), 4) == 0)) {
+                && is_windows_device_path(raw_info->img_info.images[idx])) {
             offset_to_read = (offset_to_read / raw_info->img_info.sector_size) * raw_info->img_info.sector_size;
             len_to_read += raw_info->img_info.sector_size; // this length will already be a multiple of sector size
             sector_aligned_buf = (char *)tsk_malloc(len_to_read);
@@ -630,6 +641,119 @@ get_size(const TSK_TCHAR * a_file, uint8_t a_is_winobj)
     return size;
 }
 
+#ifdef TSK_WIN32
+/**
+* \internal
+* Test seeking to the given offset and then reading a sector.
+* @param file_handle The open file handle to the image
+* @param offset      The offset to seek to (in bytes)
+* @param len         The length to read (in bytes). Should be a multiple of the sector size.
+* @param buf         An allocated buffer large enough to hold len bytes
+*
+* @return 1 if the seek/read is successful, 0 if not
+*/
+static int
+test_sector_read(HANDLE file_handle, TSK_OFF_T offset, DWORD len, char * buf) {
+    LARGE_INTEGER li;
+    li.QuadPart = offset;
+
+    // Seek to the given offset
+    li.LowPart = SetFilePointer(file_handle, li.LowPart,
+        &li.HighPart, FILE_BEGIN);
+    if ((li.LowPart == INVALID_SET_FILE_POINTER) &&
+        (GetLastError() != NO_ERROR)) {
+        return 0;
+    }
+
+    // Read a byte at the given offset
+    DWORD nread;
+    if (FALSE == ReadFile(file_handle, buf, len, &nread, NULL)) {
+        return 0;
+    }
+    if (nread != len) {
+        return 0;
+    }
+
+    // Success
+    return 1;
+}
+
+/**
+ * Attempts to calculate the actual sector size needed for reading the image.
+ * If successful, the calculated sector size will be stored in raw_info. If it
+ * fails the sector_size field will not be updated.
+ * @param raw_info    The incomplete IMG_RAW_INFO object. The sector_size field may be updated by this method.
+ * @param image_name  Image file name
+ * @param image_size  Image size
+*/
+static void
+set_device_sector_size(IMG_RAW_INFO * raw_info, const TSK_TCHAR * image_name, TSK_OFF_T image_size) {
+    unsigned int min_sector_size = 512;
+    unsigned int max_sector_size = 4096;
+
+    HANDLE file_handle = CreateFile(image_name, FILE_READ_DATA,
+        FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0,
+        NULL);
+    if (file_handle == INVALID_HANDLE_VALUE) {
+        if (tsk_verbose) {
+            tsk_fprintf(stderr,
+                "find_sector_size: failed to open image \"%" PRIttocTSK "\"\n", image_name);
+        }
+        return;
+    }
+
+    // First test whether we need to align on sector boundaries
+    char* buf = malloc(max_sector_size);
+    int needs_sector_alignment = 0;
+    if (image_size > raw_info->img_info.sector_size) {
+        if (test_sector_read(file_handle, 1, raw_info->img_info.sector_size, buf)) {
+            needs_sector_alignment = 0;
+        }
+        else {
+            needs_sector_alignment = 1;
+        }
+    }
+
+    // If reading a sector starting at offset 1 failed, the assumption is that we have a device
+    // that requires reads to be sector-aligned. 
+    if (needs_sector_alignment) {
+        // Start at the minimum (512) and double up to max_sector_size (4096)
+        unsigned int sector_size = min_sector_size;
+
+        while (sector_size <= max_sector_size) {
+            // If we don't have enough data to do the test just stop
+            if (image_size < sector_size * 2) {
+                break;
+            }
+
+            if (test_sector_read(file_handle, sector_size, sector_size, buf)) {
+                // Found a valid sector size
+                if (tsk_verbose) {
+                    tsk_fprintf(stderr,
+                        "find_sector_size: using sector size %d\n", sector_size);
+                }
+                raw_info->img_info.sector_size = sector_size;
+
+                if (file_handle != 0) {
+                    CloseHandle(file_handle);
+                }
+                free(buf);
+                return;
+            }
+            sector_size *= 2;
+        }
+        if (tsk_verbose) {
+            tsk_fprintf(stderr,
+                "find_sector_size: failed to determine correct sector size. Reverting to default %d\n", raw_info->img_info.sector_size);
+        }
+        free(buf);
+    }
+
+    if (file_handle != 0) {
+        CloseHandle(file_handle);
+    }
+}
+#endif
 
 /** 
  * \internal
@@ -661,9 +785,6 @@ raw_open(int a_num_img, const TSK_TCHAR * const a_images[],
     img_info->close = raw_close;
     img_info->imgstat = raw_imgstat;
 
-    img_info->sector_size = 512;
-    if (a_ssize)
-        img_info->sector_size = a_ssize;
     raw_info->is_winobj = 0;
 
 #if defined(TSK_WIN32) || defined(__CYGWIN__)
@@ -683,6 +804,20 @@ raw_open(int a_num_img, const TSK_TCHAR * const a_images[],
         return NULL;
     }
 
+    /* Set the sector size */
+    img_info->sector_size = 512;
+    if (a_ssize) {
+        img_info->sector_size = a_ssize;
+    }
+#ifdef TSK_WIN32
+    else if (is_windows_device_path(a_images[0])) {
+        /* On Windows, figure out the actual sector size if one was not given and this is a device.
+         * This is to prevent problems reading later. */
+        set_device_sector_size(raw_info, a_images[0], first_seg_size);
+    }
+#endif
+
+
     /* see if there are more of them... */
     if ((a_num_img == 1) && (raw_info->is_winobj == 0)) {
         if ((raw_info->img_info.images =