diff --git a/bindings/java/doxygen/Doxyfile b/bindings/java/doxygen/Doxyfile
index 380ba0c78840caebf74e4e9a3c7d30d5ed1b40ae..d10f4c37dd729cb895bbb29c194fb0ae3638b241 100644
--- a/bindings/java/doxygen/Doxyfile
+++ b/bindings/java/doxygen/Doxyfile
@@ -765,6 +765,7 @@ INPUT                  = main.dox \
                          artifact_catalog.dox \
                          insert_and_update_database.dox \
                          communications.dox \
+                         db_schema_8_6.dox \
 						 ../src
 
 # This tag can be used to specify the character encoding of the source files
diff --git a/bindings/java/doxygen/db_schema_8_6.dox b/bindings/java/doxygen/db_schema_8_6.dox
new file mode 100644
index 0000000000000000000000000000000000000000..824fa095aeb31a8072dca7841dbd1c1b1f4f9b71
--- /dev/null
+++ b/bindings/java/doxygen/db_schema_8_6.dox
@@ -0,0 +1,307 @@
+/*! \page db_schema_8_6_page TSK & Autopsy Database Schema (Schema version 8.6)
+
+
+# Introduction
+
+This page outlines version 8.6 the database that is used by The Sleuth Kit and Autopsy. The goal of this page is to provide short descriptions for each table and column and not focus on foreign key requirements, etc. If you want that level of detail, then refer to the actual schema in addition to this. 
+
+You can find a basic graphic of some of the table relationships <a href="https://docs.google.com/drawings/d/1omR_uUAp1fQt720oJ-kk8C48BXmVa3PNjPZCDdT0Tb4/edit?usp#sharing">here</a>
+
+
+Some general notes on this schema:
+
+- Nearly every type of data is assigned a unique ID, called the Object ID
+- The objects form a hierarchy, that shows where data came from.  A child comes from its parent.  
+ - For example, disk images are the root, with a volume system below it, then a file system, and then files and directories. 
+- This schema has been designed to store data beyond the file system data that The Sleuth Kit supports. It can store carved files, a folder full of local files, etc.
+- The Blackboard is used to store artifacts, which contain attributes (name/value pairs).  Artifacts are used to store data types that do not have more formal tables. Module writers can make whatever artifact types they want. See \ref mod_bbpage for more details. 
+- The Sleuth Kit will make virtual files to span the unallocated space.  They will have a naming format of 'Unalloc_[PARENT-OBJECT-ID]_[BYTE-START]_[BYTE-END]'.
+
+
+# General Information Tables 
+## tsk_db_info 
+Metadata about the database.
+- **schema_ver** - Version of the database schema used to create database (must be 2 in this case)
+- **tsk_ver** - Version of TSK used to create database
+
+## tsk_db_info_extended
+Name & Value pair table to store any information about the database.  For example, which schema it was created with. etc. 
+- **name** - Any string name
+- **value** - Any string value
+
+
+# Object Tables 
+## tsk_objects 
+Every object (image, volume system, file, etc.) has an entry in this table.  This table allows you to find the parent of a given object and allows objects to be tagged and have children.  This table provides items with a unique object id.  The details of the object are in other tables.  
+- **obj_id** - Unique id 
+- **par_obj_id** - The object id of the parent object (null for root objects). The parent of a volume system is an image, the parent of a directory is a directory or filesystem, the parent of a filesystem is a volume or an image, etc.
+- **type** - Object type (as org.sleuthkit.datamodel.TskData.ObjectType enum).
+
+
+# Data Source/Device Tables 
+## data_source_info
+Contains information about a data source, which could be an image.  This is where we group data sources into devices (based on device ID)
+- **obj_id** - Id of image/data source in tsk_objects
+- **device_id** - Unique ID (GUID) for the device that contains the data source. 
+- **time_zone** - Timezone that the data source was originally located in. 
+
+
+* Disk Image Tables
+
+## tsk_image_info 
+Contains information about each set of images that is stored in the database. 
+- **obj_id** - Id of image in tsk_objects
+- **type** - Type of disk image format (as org.sleuthkit.datamodel.TskData.TSK_IMG_TYPE_ENUM)
+- **ssize** - Sector size of device in bytes
+- **tzone** - Timezone where image is from (the same format that TSK tools want as input)
+- **size** - Size of the original image (in bytes) 
+- **md5** - Hash of the image.  Currently, this is populated only if the input image is E01. 
+- **display_name** - display name of the image. 
+
+## tsk_image_names
+Stores path(s) to file(s) on disk that make up an image set.
+- **obj_id** - Id of image in tsk_objects
+- **name** - Path to location of image file on disk
+- **sequence** - Position in sequence of image parts
+
+
+# Volume System Tables
+## tsk_vs_info
+Contains one row for every volume system found in the images.
+- **obj_id** - Id of volume system in tsk_objects
+- **vs_type** - Type of volume system / media management (as org.sleuthkit.datamodel.TskData.TSK_VS_TYPE_ENUM])
+- **img_offset** - Byte offset where VS starts in disk image
+- **block_size** - Size of blocks in bytes
+
+## tsk_vs_parts
+Contains one row for every volume / partition in the images. 
+- **obj_id** - Id of volume in tsk_objects
+- **addr** - Address of this partition
+- **start** - Sector offset of start of partition
+- **length** - Number of sectors in partition
+- **desc** - Description of partition (volume system type-specific)
+- **flags** - Flags for partition (as org.sleuthkit.datamodel.TskData.TSK_VS_PART_FLAG_ENUM])
+
+## tsk_pool_info 
+Contains information about pools (for APFS, logical disk management, etc.)
+- TODO: Fill in columns
+
+# File System Tables
+## tsk_fs_info
+Contains one for for every file system in the images. 
+- **obj_id** - Id of filesystem in tsk_objects
+- **img_offset** - Byte offset that filesystem starts at
+- **fs_type** - Type of file system (as org.sleuthkit.datamodel.TskData.TSK_FS_TYPE_ENUM])
+- **block_size** - Size of each block (in bytes)
+- **block_count** - Number of blocks in filesystem
+- **root_inum** - Metadata address of root directory
+- **first_inum** - First valid metadata address
+- **last_inum** - Last valid metadata address
+- **display_name** - Display name of file system (could be volume label) (New in V3)
+
+## tsk_files
+Contains one for for every file found in the images.  Has the basic metadata for the file. 
+- **obj_id** - Id of file in tsk_objects
+- **fs_obj_id** - Id of filesystem in tsk_objects (NULL if file is not located in a file system -- carved in unpartitioned space, etc.)
+- **type** - Type of file: filesystem, carved, etc. (as org.sleuthkit.datamodel.TskData.TSK_DB_FILES_TYPE_ENUM enum)
+- **attr_type** - Type of attribute (as org.sleuthkit.datamodel.TskData.TSK_FS_ATTR_TYPE_ENUM)
+- **attr_id** - Id of attribute
+- **name** - Name of attribute. Will be NULL if attribute doesn't have a name.  Must not have any slashes in it. 
+- **meta_addr** - Address of the metadata structure that the name points to.
+- **meta_seq** - Sequence of the metadata address - New in V3
+- **has_layout** - True if file has an entry in tsk_file_layout
+- **has_path** - True if file has an entry in tsk_files_path
+- **dir_type** - File type information: directory, file, etc. (as org.sleuthkit.datamodel.TskData.TSK_FS_NAME_TYPE_ENUM)
+- **meta_type** - File type (as org.sleuthkit.datamodel.TskData.TSK_FS_META_TYPE_ENUM)
+- **dir_flags** -  Flags that describe allocation status etc. (as org.sleuthkit.datamodel.TskData.TSK_FS_NAME_FLAG_ENUM)
+- **meta_flags** - Flags for this file for its allocation status etc. (as org.sleuthkit.datamodel.TskData.TSK_FS_META_FLAG_ENUM)
+- **size** - File size in bytes
+- **ctime** - Last file / metadata status change time (stored in number of seconds since Jan 1, 1970 UTC)
+- **crtime** - Created time
+- **atime** - Last file content accessed time
+- **mtime** - Last file content modification time
+- **mode** - Unix-style permissions (as org.sleuthkit.datamodel.TskData.TSK_FS_META_MODE_ENUM)
+- **uid** - Owner id
+- **gid** - Group id
+- **md5** - MD5 hash of file contents
+- **known** - Known status of file (as org.sleuthkit.datamodel.TskData.FileKnown)
+- **parent_path** - full path of parent folder. Must begin and end with a '/' (Note that a single '/' is valid).
+- **mime_type** - MIME type of the file content, if it has been detected. 
+
+## tsk_file_layout
+Stores the layout of a file within the image.  A file will have one or more rows in this table depending on how fragmented it was. All file types use this table (file system, carved, unallocated blocks, etc.).
+- **obj_id** - Id of file in tsk_objects
+- **sequence** - Position of this run in the file (0-based and the obj_id and sequence pair will be unique in the table)
+- **byte_start** - Byte offset of fragment relative to the start of the image file
+- **byte_len** - Length of fragment in bytes
+
+
+## tsk_files_path
+If a "locally-stored" file has been imported into the database for analysis, then this table stores its path.  Used for derived files and other files that are not directly in the image file.
+- **obj_id** - Id of file in tsk_objects
+- **path** - Path to where the file is locally stored in a file system.
+- **encoding_type** - Method used to store the file on the disk. 
+
+## file_encoding_types 
+Methods that can be used to store files on local disks to prevent them from being quarantined by antivirus
+- **encoding_type** - ID of method used to store data.  See org.sleuthkit.datamodel.TskData.EncodingType enum. 
+- **name** -  Display name of technique. 
+
+## tsk_files_derived_method
+Derived files are those that result from analyzing another file.  For example, files that are extracted from a ZIP file will be considered derived.  This table keeps track of the derivation techniques that were used to make the derived files. 
+
+NOTE: This table is not used in any code.
+
+- **derived_id** - Unique id for this derivation method. 
+- **tool_name** - Name of derivation method/tool
+- **tool_version** - Version of tool used in derivation method
+- **other** - Other details
+
+## tsk_files_derived
+Each derived file has a row that captures the information needed to re-derive it
+
+NOTE: This table is not used in any code.
+
+- **obj_id** - Id of file in tsk_objects
+- **derived_id** - Id of derivation method in tsk_files_derived_method
+- **rederive** - Details needed to re-derive file (will be specific to the derivation method)
+
+
+# Blackboard Tables 
+The \ref mod_bbpage is used to store results from analysis modules. 
+
+## blackboard_artifacts
+Stores artifacts associated with objects.
+- **artifact_id** - Id of the artifact (assigned by the database)
+- **obj_id** - Id of the associated object
+- **artifact_type_id** - Id for the type of artifact (can be looked up in the blackboard_artifact_types table)
+
+## blackboard_attributes
+Stores name value pairs associated with an artifact. Only one of the value columns should be populated
+- **artifact_id** - Id of the associated artifact.
+- **source** - Source string, should be module name that created the entry.
+- **context** - Additional context string
+- **attribute_type_id** - Id for the type of attribute (can be looked up in the blackboard_attribute_types)
+- **value_type** - The type of value (0 for string, 1 for int, 2 for long, 3 for double, 4 for byte array)
+- **value_byte** - A blob of binary data (should be empty unless the value type is byte)
+- **value_text** - A string of text (should be empty unless the value type is string)
+- **value_int32** - An integer (should be 0 unless the value type is int)
+- **value_int64** - A long integer (should be 0 unless the value type is long)
+- **value_double** - A double (should be 0.0 unless the value type is double)
+
+## blackboard_artifact_types
+Types of artifacts
+- **artifact_type_id** - Id for the type (this is used by the blackboard_artifacts table)
+- **type_name** - A string identifier for the type (unique)
+- **display_name** - A display name for the type (not unique, should be human readable)
+
+
+## blackboard_attribute_types
+Types of attribute
+- **attribute_type_id** - Id for the type (this is used by the blackboard_attributes table)
+- **type_name** - A string identifier for the type (unique)
+- **display_name - A display name for the type (not unique, should be human readable)
+
+
+
+# Communication Accounts
+TODO
+
+\ref mod_compage
+
+## accounts
+
+## account_types
+
+## account_relationships
+
+
+# Timeline
+TODO
+## tsk_event_types
+
+## tsk_event_descriptions
+
+## tsk_events
+
+
+# Examiners and Reports
+
+TODO
+
+## tsk_examiners
+
+## reports
+
+
+# Tags 
+
+## tag_names table
+Defines what tag names the user has created and can therefore be applied.
+- tag_name_id - Unique ID for each tag name
+- display_name - Display name of tag
+- description  - Description  (can be empty string)
+- color - Color choice for tag (can be empty string)
+
+## tsk_tag_sets
+TODO
+
+## content_tags table
+One row for each file tagged.  
+- tag_id - unique ID
+- obj_id - object id of Content that has been tagged
+- tag_name_id - Tag name that was used
+- comment  - optional comment 
+- begin_byte_offset - optional byte offset into file that was tagged
+- end_byte_offset - optional byte ending offset into file that was tagged
+
+## blackboard_artifact_tags table
+One row for each artifact that is tagged.
+- tag_id - unique ID
+- artifact_id - Artifact ID of artifact that was tagged
+- tag_name_id - Tag name that was used
+- comment - optional comment
+
+
+# Ingest Module Status
+These tables keep track in Autopsy which modules were run on the data sources.
+
+TODO
+
+## ingest_module_types table
+Defines the types of ingest modules supported. 
+- type_id 
+- type_name 
+
+## ingest_modules
+Defines which modules were installed.  One row for each module. 
+- ingest_module_id 
+- display_name 
+- unique_name 
+- type_id 
+- version 
+
+## ingest_job_status_types table
+- type_id 
+- type_name 
+
+
+##  ingest_jobs
+One row is created each time ingest is started, which is a set of modules in a pipeline. 
+- ingest_job_id 
+- obj_id 
+- host_name 
+- start_date_time 
+- end_date_time 
+- status_id 
+- settings_dir 
+
+##  ingest_job_modules
+Defines the order of the modules in a given pipeline (i.e. ingest_job)
+- ingest_job_id 
+- ingest_module_id 
+- pipeline_position 
+
+
+
+*/
diff --git a/bindings/java/doxygen/main.dox b/bindings/java/doxygen/main.dox
index 45314c50f742d85bacc6315223297ac3f44f7de1..234245d78768d7aacc30f6a4531a08785424c526 100644
--- a/bindings/java/doxygen/main.dox
+++ b/bindings/java/doxygen/main.dox
@@ -40,6 +40,12 @@ You can also access the data in its tree form by starting with org.sleuthkit.dat
 - \subpage mod_bbpage is where analysis modules (such as those in Autopsy) can post and save their results. 
 - The \subpage artifact_catalog_page gives a list of the current artifacts and attributes used on \ref mod_bbpage.
 - \subpage mod_compage is where analysis modules can store and retrieve communications-related data. 
+
+\section main_db Database Topics
+The Sleuth Kit has its own database schema that is shared with Autopsy and other tools. The primary way it gets populated is via the Java code. 
+
+- Database Schema Documentation:
+ - \subpage db_schema_8_6_page 
 - Refer to \subpage query_database_page if you are going to use one of the SleuthkitCase methods that requires you to specify a query. 
 - Refer to \subpage insert_and_update_database_page if you are a Sleuth Kit developer and want to avoid database issues.
 
diff --git a/bindings/java/nbproject/project.xml b/bindings/java/nbproject/project.xml
index 57a2b6befa984bdfa69a539c68c6200745375049..2b34d8ff45723f61d2970a892b13cf86a71e8cfb 100755
--- a/bindings/java/nbproject/project.xml
+++ b/bindings/java/nbproject/project.xml
@@ -114,14 +114,14 @@
         <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-42.2.18.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>
+                <classpath mode="compile">lib;lib/diffutils-1.2.1.jar;lib/junit-4.12.jar;lib/postgresql-42.2.18.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>
             <compilation-unit>
                 <package-root>test</package-root>
                 <unit-tests/>
-                <classpath mode="compile">build;lib/diffutils-1.2.1.jar;lib/diffutils-1.2.1-javadoc.jar;lib/diffutils-1.2.1-sources.jar;lib/junit-4.8.2.jar</classpath>
+                <classpath mode="compile">build;lib/diffutils-1.2.1.jar;lib/diffutils-1.2.1-javadoc.jar;lib/diffutils-1.2.1-sources.jar;lib/junit-4.12.jar</classpath>
                 <built-to>build</built-to>
                 <built-to>test</built-to>
                 <source-level>1.8</source-level>
diff --git a/bindings/java/src/org/sleuthkit/datamodel/AbstractAttribute.java b/bindings/java/src/org/sleuthkit/datamodel/AbstractAttribute.java
new file mode 100644
index 0000000000000000000000000000000000000000..a5f35d1df8b752f7f21d253748a62a3ef575df10
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/AbstractAttribute.java
@@ -0,0 +1,403 @@
+/*
+ * Sleuth Kit Data Model
+ *
+ * Copyright 2021 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 java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Attributes are a name-value pairs. Abstract Attribute provides the base
+ * functionality for a name value pair with type safety (analogous to a C union)
+ *
+ */
+abstract class AbstractAttribute {
+
+	private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); 
+
+	private BlackboardAttribute.Type attributeType;
+
+	private final int valueInt;
+	private final long valueLong;
+	private final double valueDouble;
+	private final String valueString;
+	private final byte[] valueBytes;
+
+	private SleuthkitCase sleuthkitCase;
+
+	private long attributeOwnerId;
+
+	
+	/**
+	 * Constructs an attribute with an integer value. The attribute should be
+	 * added to an appropriate artifact.
+	 *
+	 * @param attributeType The attribute type.
+	 * @param valueInt      The attribute value.
+	 *
+	 * @throws IllegalArgumentException If the value type of the specified
+	 *                                  attribute type is not
+	 *                                  TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.INTEGER.
+	 */
+	public AbstractAttribute(BlackboardAttribute.Type attributeType, int valueInt) {
+		if (attributeType.getValueType() != BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.INTEGER) {
+			throw new IllegalArgumentException("Type mismatched with value type");
+		}
+		this.attributeOwnerId = 0;
+		this.attributeType = attributeType;
+		this.valueInt = valueInt;
+		this.valueLong = 0;
+		this.valueDouble = 0;
+		this.valueString = "";
+		this.valueBytes = new byte[0];
+	}
+
+
+	/**
+	 * Constructs an attribute with a long/datetime value. The attribute should
+	 * be added to an appropriate artifact.
+	 *
+	 * @param attributeType The attribute type.
+	 * @param valueLong     The attribute value.
+	 *
+	 * @throws IllegalArgumentException If the value type of the specified
+	 *                                  standard attribute type is not
+	 *                                  TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.LONG
+	 *                                  or
+	 *                                  TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DATETIME.
+	 */
+	public AbstractAttribute(BlackboardAttribute.Type attributeType, long valueLong) {
+		if (attributeType.getValueType() != BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.LONG
+				&& attributeType.getValueType() != BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DATETIME) {
+			throw new IllegalArgumentException("Type mismatched with value type");
+		}
+		this.attributeOwnerId = 0;
+		this.attributeType = attributeType;
+		this.valueInt = 0;
+		this.valueLong = valueLong;
+		this.valueDouble = 0;
+		this.valueString = "";
+		this.valueBytes = new byte[0];
+	}
+
+
+	/**
+	 * Constructs an attribute with a double value. The attribute should be
+	 * added to an appropriate artifact.
+	 *
+	 * @param attributeType The attribute type.
+	 * @param valueDouble   The attribute value.
+	 *
+	 * @throws IllegalArgumentException If the value type of the specified
+	 *                                  attribute type is not
+	 *                                  TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DOUBLE.
+	 */
+	public AbstractAttribute(BlackboardAttribute.Type attributeType, double valueDouble) {
+		if (attributeType.getValueType() != BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DOUBLE) {
+			throw new IllegalArgumentException("Type mismatched with value type");
+		}
+		this.attributeOwnerId = 0;
+		this.attributeType = attributeType;
+		this.valueInt = 0;
+		this.valueLong = 0;
+		this.valueDouble = valueDouble;
+		this.valueString = "";
+		this.valueBytes = new byte[0];
+	}
+
+
+	/**
+	 * Constructs an attribute with a string value. The attribute should be
+	 * added to an appropriate artifact.
+	 *
+	 * @param attributeType The attribute type.
+	 * @param valueString   The attribute value.
+	 *
+	 * @throws IllegalArgumentException If the value type of the specified
+	 *                                  attribute type is not
+	 *                                  TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING.
+	 */
+	public AbstractAttribute(BlackboardAttribute.Type attributeType, String valueString) {
+		if (attributeType.getValueType() != BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING
+				&& attributeType.getValueType() != BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON) {
+			throw new IllegalArgumentException("Type mismatched with value type");
+		}
+		this.attributeOwnerId = 0;
+		this.attributeType = attributeType;
+		this.valueInt = 0;
+		this.valueLong = 0;
+		this.valueDouble = 0;
+		if (valueString == null) {
+			this.valueString = "";
+		} else {
+			this.valueString = replaceNulls(valueString).trim();
+		}
+		this.valueBytes = new byte[0];
+	}
+
+
+	/**
+	 * Constructs an attribute with a byte array value. The attribute should be
+	 * added to an appropriate artifact.
+	 *
+	 * @param attributeType The attribute type.
+	 * @param valueBytes    The attribute value.
+	 *
+	 * @throws IllegalArgumentException If the value type of the specified
+	 *                                  attribute type is not
+	 *                                  TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.BYTE.
+	 */
+	public AbstractAttribute(BlackboardAttribute.Type attributeType, byte[] valueBytes) {
+		if (attributeType.getValueType() != BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.BYTE) {
+			throw new IllegalArgumentException("Type mismatched with value type");
+		}
+		this.attributeOwnerId = 0;
+		this.attributeType = attributeType;
+		this.valueInt = 0;
+		this.valueLong = 0;
+		this.valueDouble = 0;
+		this.valueString = "";
+		if (valueBytes == null) {
+			this.valueBytes = new byte[0];
+		} else {
+			this.valueBytes = valueBytes;
+		}
+	}
+
+	/**
+	 * Constructs an artifact attribute. To be used when creating an attribute
+	 * based on a query of the blackboard _attributes table in the case
+	 * database.
+	 *
+	 * @param attributeOwnerId The owner id for this attribute.
+	 * @param attributeTypeID  The attribute type id. 
+	 * @param valueType        The attribute value type.
+	 * @param valueInt         The value from the the value_int32 column.
+	 * @param valueLong        The value from the the value_int64 column.
+	 * @param valueDouble      The value from the the value_double column.
+	 * @param valueString      The value from the the value_text column.
+	 * @param valueBytes       The value from the the value_byte column.
+	 * @param sleuthkitCase    A reference to the SleuthkitCase object
+	 *                         representing the case database.
+	 */
+	AbstractAttribute(long attributeOwnerId, BlackboardAttribute.Type attributeType,
+			int valueInt, long valueLong, double valueDouble, String valueString, byte[] valueBytes,
+			SleuthkitCase sleuthkitCase) {
+
+		this.attributeOwnerId = attributeOwnerId;
+		this.attributeType = attributeType;
+		this.valueInt = valueInt;
+		this.valueLong = valueLong;
+		this.valueDouble = valueDouble;
+		if (valueString == null) {
+			this.valueString = "";
+		} else {
+			this.valueString = replaceNulls(valueString).trim();
+		}
+		if (valueBytes == null) {
+			this.valueBytes = new byte[0];
+		} else {
+			this.valueBytes = valueBytes;
+		}
+		this.sleuthkitCase = sleuthkitCase;
+	}
+
+	/**
+	 * Gets the attribute value as a string, formatted as required.
+	 *
+	 * @return The value as a string.
+	 */
+	public String getDisplayString() {
+		switch (attributeType.getValueType()) {
+			case STRING:
+				return getValueString();
+			case INTEGER:
+				if (attributeType.getTypeID() == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_READ_STATUS.getTypeID()) {
+					if (getValueInt() == 0) {
+						return "Unread";
+					} else {
+						return "Read";
+					}
+				}
+				return Integer.toString(getValueInt());
+			case LONG: 
+				return Long.toString(getValueLong());
+			case DOUBLE:
+				return Double.toString(getValueDouble());
+			case BYTE:
+				return bytesToHexString(getValueBytes());
+			case DATETIME: 
+				// once we have TSK timezone, that should be used here.
+				return TimeUtilities.epochToTime(getValueLong());
+			case JSON: {
+				return getValueString();
+			}
+		}
+		return "";
+	}
+
+	/**
+	 * Gets the type of this attribute.
+	 *
+	 * @return The attribute type.
+	 */
+	public BlackboardAttribute.Type getAttributeType() {
+		return this.attributeType;
+	}
+
+	/**
+	 * Gets the value type.
+	 *
+	 * @return The value type
+	 */
+	public BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE getValueType() {
+		return attributeType.getValueType();
+	}
+
+	/**
+	 * Gets the value of this attribute. The value is only valid if the
+	 * attribute value type is TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.INTEGER.
+	 *
+	 * @return The attribute value.
+	 */
+	public int getValueInt() {
+		return valueInt;
+	}
+
+	/**
+	 * Gets the value of this attribute. The value is only valid if the
+	 * attribute value type is TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.LONG.
+	 *
+	 * @return The attribute value.
+	 */
+	public long getValueLong() {
+		return valueLong;
+	}
+
+	/**
+	 * Gets the value of this attribute. The value is only valid if the
+	 * attribute value type is TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DOUBLE.
+	 *
+	 * @return The attribute value.
+	 */
+	public double getValueDouble() {
+		return valueDouble;
+	}
+
+	/**
+	 * Gets the value of this attribute. The value is only valid if the
+	 * attribute value type is TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING or
+	 * TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON.
+	 *
+	 * @return The attribute value.
+	 */
+	public String getValueString() {
+		return valueString;
+	}
+
+	/**
+	 * Gets the value of this attribute. The value is only valid if the
+	 * attribute value type is TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.BYTE.
+	 *
+	 * @return The attribute value.
+	 */
+	public byte[] getValueBytes() {
+		return Arrays.copyOf(valueBytes, valueBytes.length);
+	}
+
+	/**
+	 * Gets the owner Id of this attribute. An owner is defined as the Object
+	 * to which this attribute is associated with.
+	 * Eg: For a file Attribute, the owner id would be the file object id.
+	 *
+	 * Each implementation is expected to give a more specific name. 
+	 *
+	 * @return
+	 */
+	long getAttributeOwnerId() {
+		return this.attributeOwnerId;
+	}
+
+	SleuthkitCase getCaseDatabase() {
+		return this.sleuthkitCase;
+	}
+
+	/**
+	 * Sets the reference to the SleuthkitCase object that represents the case
+	 * database.
+	 *
+	 * @param sleuthkitCase A reference to a SleuthkitCase object.
+	 */
+	void setCaseDatabase(SleuthkitCase sleuthkitCase) {
+		this.sleuthkitCase = sleuthkitCase;
+	}
+
+	/**
+	 * Sets the owner id for this attribute.
+	 *
+	 * @param attributeOwnerId The attribute owner id.
+	 */
+	void setAttributeOwnerId(long attributeOwnerId) {
+		this.attributeOwnerId = attributeOwnerId;
+	}
+
+ 
+	/**
+	 * Converts a byte array to a string.
+	 *
+	 * @param bytes The byte array.
+	 *
+	 * @return The string.
+	 */
+	static String bytesToHexString(byte[] bytes) {
+		// from http://stackoverflow.com/questions/9655181/convert-from-byte-array-to-hex-string-in-java
+		char[] hexChars = new char[bytes.length * 2];
+		for (int j = 0; j < bytes.length; j++) {
+			int v = bytes[j] & 0xFF;
+			hexChars[j * 2] = HEX_ARRAY[v >>> 4];
+			hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
+		}
+		return new String(hexChars);
+	}
+	
+	/**
+	 * Replace all NUL characters in the string with the SUB character
+	 *
+	 * @param text The input string.
+	 *
+	 * @return The output string.
+	 */
+	final String replaceNulls(String text) {
+		return text.replace((char) 0x00, (char) 0x1A);
+	}
+
+
+	boolean isAttributeEquals(Object that) {
+		if (that instanceof AbstractAttribute) {
+			AbstractAttribute other = (AbstractAttribute) that;
+			Object[] thisObject = new Object[]{this.getAttributeType(), this.getValueInt(), this.getValueLong(), this.getValueDouble(),
+				this.getValueString(), this.getValueBytes()};
+			Object[] otherObject = new Object[]{other.getAttributeType(), other.getValueInt(), other.getValueLong(), other.getValueDouble(),
+				other.getValueString(), other.getValueBytes()};
+
+			return Objects.deepEquals(thisObject, otherObject);
+		} else {
+			return false;
+		}
+	}
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/AbstractContent.java b/bindings/java/src/org/sleuthkit/datamodel/AbstractContent.java
index 50afea2be2b2dc50a86ce6f58d27f7789ab3662a..eec894d2bd1581243cd0e6a25d33c3983c464831 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/AbstractContent.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/AbstractContent.java
@@ -19,14 +19,17 @@
 package org.sleuthkit.datamodel;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.logging.Level;
 import java.util.logging.Logger;
+import org.sleuthkit.datamodel.Blackboard.BlackboardException;
 import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE;
 import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE;
+import org.sleuthkit.datamodel.SleuthkitCase.CaseDbTransaction;
 import org.sleuthkit.datamodel.SleuthkitCase.ObjectInfo;
 
 /**
@@ -323,6 +326,22 @@ public BlackboardArtifact newArtifact(int artifactTypeID) throws TskCoreExceptio
 		return db.newBlackboardArtifact(artifactTypeID, objId);
 	}
 
+	@Override
+	public AnalysisResultAdded newAnalysisResult(BlackboardArtifact.Type artifactType, Score score, String conclusion, String configuration, String justification, Collection<BlackboardAttribute> attributesList) throws TskCoreException {
+		
+		CaseDbTransaction trans = db.beginTransaction();
+		try {
+			AnalysisResultAdded resultAdded = db.getBlackboard().newAnalysisResult(artifactType, objId, this.getDataSource().getId(), score, conclusion, configuration, justification, attributesList, trans);
+			
+			trans.commit();
+			return resultAdded;
+		}
+		catch (BlackboardException ex) {
+			trans.rollback();
+			throw new TskCoreException(String.format("Error adding analysis result to content with objId = %d.", objId), ex);
+		}
+	}
+
 	@Override
 	public BlackboardArtifact newArtifact(BlackboardArtifact.ARTIFACT_TYPE type) throws TskCoreException {
 		return newArtifact(type.getTypeID());
@@ -408,6 +427,21 @@ public ArrayList<BlackboardArtifact> getAllArtifacts() throws TskCoreException {
 		return db.getMatchingArtifacts("WHERE obj_id = " + objId); //NON-NLS
 	}
 
+	@Override
+	public List<AnalysisResult> getAllAnalysisResults() throws TskCoreException {
+		return db.getBlackboard().getAnalysisResults(objId);
+	}
+	
+	@Override
+	public Score getAggregateScore() throws TskCoreException {
+		return db.getScoringManager().getAggregateScore(objId);
+	}
+
+	@Override
+	public List<AnalysisResult> getAnalysisResults(BlackboardArtifact.Type artifactType) throws TskCoreException {
+		return db.getBlackboard().getAnalysisResults(objId, artifactType.getTypeID()); //NON-NLS
+	}
+	
 	@Override
 	public long getArtifactsCount(String artifactTypeName) throws TskCoreException {
 		return db.getBlackboardArtifactsCount(artifactTypeName, objId);
diff --git a/bindings/java/src/org/sleuthkit/datamodel/AbstractFile.java b/bindings/java/src/org/sleuthkit/datamodel/AbstractFile.java
index 4e2f90a85e3ec03c998ec518bb02201fd04d739e..38cfeec4e8e1138765375672a700717066bcc720 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/AbstractFile.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/AbstractFile.java
@@ -25,7 +25,10 @@
 import java.sql.Statement;
 import java.text.MessageFormat;
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 import java.util.ResourceBundle;
 import java.util.Set;
 import java.util.SortedSet;
@@ -90,6 +93,8 @@ public abstract class AbstractFile extends AbstractContent {
 	private static final ResourceBundle BUNDLE = ResourceBundle.getBundle("org.sleuthkit.datamodel.Bundle");
 	private long dataSourceObjectId;
 	private final String extension;
+	private final List<Attribute> fileAttributesCache = new ArrayList<Attribute>();
+	private boolean loadedAttributesCacheFromDb = false;
 
 	/**
 	 * Initializes common fields used by AbstactFile implementations (objects in
@@ -144,7 +149,8 @@ public abstract class AbstractFile extends AbstractContent {
 			String md5Hash, String sha256Hash, FileKnown knownState,
 			String parentPath,
 			String mimeType,
-			String extension) {
+			String extension, 
+			List<Attribute> fileAttributes) {
 		super(db, objId, name);
 		this.dataSourceObjectId = dataSourceObjectId;
 		this.attrType = attrType;
@@ -176,6 +182,10 @@ public abstract class AbstractFile extends AbstractContent {
 		this.mimeType = mimeType;
 		this.extension = extension == null ? "" : extension;
 		this.encodingType = TskData.EncodingType.NONE;
+		if (Objects.nonNull(fileAttributes) && !fileAttributes.isEmpty()) {
+			this.fileAttributesCache.addAll(fileAttributes);
+			loadedAttributesCacheFromDb = true;
+		}
 	}
 
 	/**
@@ -499,8 +509,78 @@ public void setSha256Hash(String sha256Hash) {
 	 */
 	public String getSha256Hash() {
 		return this.sha256Hash;
+	}	
+	
+	/**
+	 * Gets the attributes of this File
+	 * @return
+	 * @throws TskCoreException 
+	 */
+	public List<Attribute> getAttributes() throws TskCoreException {
+		synchronized (this) {
+			if (!loadedAttributesCacheFromDb) {
+				ArrayList<Attribute> attributes = getSleuthkitCase().getFileAttributes(this);
+				fileAttributesCache.clear();
+				fileAttributesCache.addAll(attributes);
+				loadedAttributesCacheFromDb = true;
+			}
+			return Collections.unmodifiableList(fileAttributesCache);
+		}
 	}
 
+	/**
+	 * Adds a collection of attributes to this file in a single operation 
+	 * within a transaction supplied by the caller.
+	 *
+	 * @param attributes        The collection of attributes.
+	 * @param caseDbTransaction The transaction in the scope of which the
+	 *                          operation is to be performed, managed by the
+	 *                          caller. if Null is passed in a local transaction
+	 *							will be created and used. 
+	 *
+	 * @throws TskCoreException         If an error occurs and the attributes
+	 *                                  were not added to the artifact.
+	 * @throws IllegalArgumentException If <code>attributes</code> is
+	 *                                  null or empty.
+	 */
+	public void addAttributes(Collection<Attribute> attributes, final SleuthkitCase.CaseDbTransaction caseDbTransaction) throws TskCoreException {
+
+		if (Objects.isNull(attributes) || attributes.isEmpty()) {
+			throw new IllegalArgumentException("null or empty attributes passed to addAttributes");
+		}
+		boolean isLocalTransaction = Objects.isNull(caseDbTransaction);
+		SleuthkitCase.CaseDbTransaction localTransaction = isLocalTransaction ? getSleuthkitCase().beginTransaction() : null;		
+		SleuthkitCase.CaseDbConnection connection = isLocalTransaction ? localTransaction.getConnection() : caseDbTransaction.getConnection();
+		
+		try {
+			for (final Attribute attribute : attributes) {
+				attribute.setAttributeOwnerId(getId()); 
+				attribute.setCaseDatabase(getSleuthkitCase());
+				getSleuthkitCase().addFileAttribute(attribute, connection);
+			}
+			
+			if(isLocalTransaction) {
+				localTransaction.commit();
+				localTransaction = null;
+			}
+			// append the new attributes if cache is already loaded.
+			synchronized (this) {
+				if (loadedAttributesCacheFromDb) {
+					fileAttributesCache.addAll(attributes);
+				}
+			}
+		} catch (SQLException ex) {
+			if (isLocalTransaction && null != localTransaction) {
+				try {
+					localTransaction.rollback();
+				} catch (TskCoreException ex2) {
+					LOGGER.log(Level.SEVERE, "Failed to rollback transaction after exception", ex2);
+				}
+			}
+			throw new TskCoreException("Error adding file attributes", ex);
+		}
+	}
+	
 	/**
 	 * Sets the known state for this file. Passed in value will be ignored if it
 	 * is "less" than the current state. A NOTABLE file cannot be downgraded to
@@ -1282,7 +1362,7 @@ protected AbstractFile(SleuthkitCase db, long objId, TskData.TSK_FS_ATTR_TYPE_EN
 			TSK_FS_NAME_TYPE_ENUM dirType, TSK_FS_META_TYPE_ENUM metaType, TSK_FS_NAME_FLAG_ENUM dirFlag, short metaFlags,
 			long size, long ctime, long crtime, long atime, long mtime, short modes, int uid, int gid, String md5Hash, FileKnown knownState,
 			String parentPath) {
-		this(db, objId, db.getDataSourceObjectId(objId), attrType, (int) attrId, name, fileType, metaAddr, metaSeq, dirType, metaType, dirFlag, metaFlags, size, ctime, crtime, atime, mtime, modes, uid, gid, md5Hash, null, knownState, parentPath, null, null);
+		this(db, objId, db.getDataSourceObjectId(objId), attrType, (int) attrId, name, fileType, metaAddr, metaSeq, dirType, metaType, dirFlag, metaFlags, size, ctime, crtime, atime, mtime, modes, uid, gid, md5Hash, null, knownState, parentPath, null, null, Collections.emptyList());
 	}
 
 	/**
@@ -1327,7 +1407,7 @@ protected AbstractFile(SleuthkitCase db, long objId, TskData.TSK_FS_ATTR_TYPE_EN
 			String name, TskData.TSK_DB_FILES_TYPE_ENUM fileType, long metaAddr, int metaSeq, TSK_FS_NAME_TYPE_ENUM dirType, TSK_FS_META_TYPE_ENUM metaType,
 			TSK_FS_NAME_FLAG_ENUM dirFlag, short metaFlags, long size, long ctime, long crtime, long atime, long mtime, short modes,
 			int uid, int gid, String md5Hash, FileKnown knownState, String parentPath, String mimeType) {
-		this(db, objId, dataSourceObjectId, attrType, (int) attrId, name, fileType, metaAddr, metaSeq, dirType, metaType, dirFlag, metaFlags, size, ctime, crtime, atime, mtime, modes, uid, gid, md5Hash, null, knownState, parentPath, null, null);
+		this(db, objId, dataSourceObjectId, attrType, (int) attrId, name, fileType, metaAddr, metaSeq, dirType, metaType, dirFlag, metaFlags, size, ctime, crtime, atime, mtime, modes, uid, gid, md5Hash, null, knownState, parentPath, null, null, Collections.emptyList());
 	}
 
 	/**
diff --git a/bindings/java/src/org/sleuthkit/datamodel/AggregateScoresChangedEvent.java b/bindings/java/src/org/sleuthkit/datamodel/AggregateScoresChangedEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..8823cb00d1338029040ac1d84c5f317fafa4fa72
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/AggregateScoresChangedEvent.java
@@ -0,0 +1,54 @@
+/*
+ * 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 com.google.common.collect.ImmutableSet;
+
+/**
+ * Event to indicate that aggregate score of objects has changed.
+ */
+final public class AggregateScoresChangedEvent implements TskDataSourceEvent, TskEvent {
+
+	AggregateScoresChangedEvent(long dataSourceId, ImmutableSet<ScoreChange> scoreChanges) {
+		this.dataSourceId = dataSourceId;
+		this.scoreChanges = scoreChanges;
+		
+		// ensure that all score changes have the same data source as the one in the event.
+		scoreChanges.stream()
+				.forEach(chg -> {
+					if (chg.getDataSourceObjectId() != dataSourceId) {
+						throw new IllegalArgumentException("ScoreChange datasource id does not match the datasource id of the event.");
+					}
+				});
+	}
+
+	private final long dataSourceId;
+	private final ImmutableSet<ScoreChange> scoreChanges;
+
+	@Override
+	public long getDataSourceId() {
+		return dataSourceId;
+	}
+
+	public ImmutableSet<ScoreChange> getScoreChanges() {
+		return scoreChanges;
+	}
+
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/AnalysisResult.java b/bindings/java/src/org/sleuthkit/datamodel/AnalysisResult.java
new file mode 100644
index 0000000000000000000000000000000000000000..2a3c629ea4840c0d0ed77ff728f98c9b2dffcc40
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/AnalysisResult.java
@@ -0,0 +1,105 @@
+/*
+ * 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;
+
+/**
+ * Analysis result is a category of artifact types that represent the outcome of
+ * some analysis technique applied to extracted data.
+ *
+ */
+public class AnalysisResult extends BlackboardArtifact {
+
+	private final String conclusion;	// conclusion of analysis - may be an empty string
+	private final Score score;			// score from the analysis
+	private final String configuration; // name of a configuration file/element that guides this analysis, may be an empty string.
+	private final String justification;  // justification/explanation from the analysis, may be an empty string.
+
+	private boolean ignoreResult = false; // ignore this analysis result when computing score of the parent object.
+
+	AnalysisResult( SleuthkitCase sleuthkitCase, long artifactID, long sourceObjId, long artifactObjId, long dataSourceObjId, int artifactTypeID, String artifactTypeName, String displayName, ReviewStatus reviewStatus, Score score, String conclusion, String configuration, String justification) {
+		super(sleuthkitCase, artifactID, sourceObjId, artifactObjId, dataSourceObjId, artifactTypeID, artifactTypeName, displayName, reviewStatus);
+		this.score = score;
+		this.conclusion = (conclusion != null) ? conclusion : "";
+		this.configuration = (configuration != null) ? configuration : "";
+		this.justification = (justification != null) ? justification : "";
+	}
+
+	AnalysisResult(SleuthkitCase sleuthkitCase, long artifactID, long sourceObjId, long artifactObjID, long dataSourceObjID, int artifactTypeID, String artifactTypeName, String displayName, ReviewStatus reviewStatus, boolean isNew, Score score, String conclusion, String configuration, String justification) {
+		super(sleuthkitCase, artifactID, sourceObjId, artifactObjID, dataSourceObjID, artifactTypeID, artifactTypeName, displayName, reviewStatus, isNew);
+		this.score = score;
+		this.conclusion = (conclusion != null) ? conclusion : "";
+		this.configuration = (configuration != null) ? configuration : "";
+		this.justification = (justification != null) ? justification : "";
+	}
+
+	/**
+	 * Returns analysis result conclusion.
+	 *
+	 * @return Conclusion, returns an empty string if not set.
+	 */
+	public String getConclusion() {
+		return conclusion;
+	}
+
+	/**
+	 * Returns analysis result score.
+	 *
+	 * @return Score.
+	 */
+	public Score getScore() {
+		return score;
+	}
+
+	/**
+	 * Returns configuration used in analysis.
+	 *
+	 * @return Configuration, returns an empty string if not set.
+	 */
+	public String getConfiguration() {
+		return configuration;
+	}
+
+	/**
+	 * Returns analysis justification.
+	 *
+	 * @return justification, returns an empty string if not set.
+	 */
+	public String getJustification() {
+		return justification;
+	}
+
+	/**
+	 * Sets if this result is to be ignored.
+	 *
+	 * @param ignore if the result should be ignored or not.
+	 */
+	public void setIgnoreResult(boolean ignore) {
+		ignoreResult = ignore;
+	}
+
+	/**
+	 * Checks if this result is to be ignored.
+	 *
+	 * @return true is the result should should be ignored, false otherwise.
+	 */
+	public boolean ignoreResult() {
+		return ignoreResult;
+	}
+
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/AnalysisResultAdded.java b/bindings/java/src/org/sleuthkit/datamodel/AnalysisResultAdded.java
new file mode 100644
index 0000000000000000000000000000000000000000..87c17ea4c747108d24627420d76e478d66f22a66
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/AnalysisResultAdded.java
@@ -0,0 +1,43 @@
+/**
+ * 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;
+
+/**
+ * This class encapsulates an analysis result added to Content, and the content's 
+ * aggregate score upon adding the analysis result. 
+ */
+public class AnalysisResultAdded {
+	
+	private final AnalysisResult analysisResult;
+	private final Score score;
+	
+	AnalysisResultAdded(AnalysisResult analysisResult, Score score) {
+		this.analysisResult = analysisResult;
+		this.score = score;
+	}
+	
+	public AnalysisResult getAnalysisResult() {
+		return analysisResult;
+	}
+
+	public Score getAggregateScore() {
+		return score;
+	}
+	
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/Attribute.java b/bindings/java/src/org/sleuthkit/datamodel/Attribute.java
new file mode 100644
index 0000000000000000000000000000000000000000..ad6e436c08377a95df1de1781e009f57dac2644d
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/Attribute.java
@@ -0,0 +1,176 @@
+/*
+ * Sleuth Kit Data Model
+ *
+ * Copyright 2021 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *	 http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.datamodel;
+
+import com.google.common.base.MoreObjects;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * This is a concrete implementation of a simple Attribute Type.
+ */
+public class Attribute extends AbstractAttribute{
+ 
+	/**
+	 * Constructs an attribute with an integer value. The attribute should be
+	 * added to an appropriate artifact.
+	 *
+	 * @param attributeType The attribute type.
+	 * @param valueInt      The attribute value.
+	 *
+	 * @throws IllegalArgumentException If the value type of the specified
+	 *                                  attribute type is not
+	 *                                  TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.INTEGER.
+	 */
+	public Attribute(BlackboardAttribute.Type attributeType, int valueInt) throws IllegalArgumentException {
+		super(attributeType, valueInt);
+	}
+
+ 
+	/**
+	 * Constructs an attribute with a long/datetime value. The attribute should
+	 * be added to an appropriate artifact.
+	 *
+	 * @param attributeType The attribute type.
+	 * @param valueLong     The attribute value.
+	 *
+	 * @throws IllegalArgumentException If the value type of the specified
+	 *                                  standard attribute type is not
+	 *                                  TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.LONG
+	 *                                  or
+	 *                                  TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DATETIME.
+	 */
+	public Attribute(BlackboardAttribute.Type attributeType, long valueLong) throws IllegalArgumentException {
+		super(attributeType, valueLong);
+	}
+
+
+	/**
+	 * Constructs an attribute with a double value. The attribute should be
+	 * added to an appropriate artifact.
+	 *
+	 * @param attributeType The attribute type.
+	 * @param valueDouble   The attribute value.
+	 *
+	 * @throws IllegalArgumentException If the value type of the specified
+	 *                                  attribute type is not
+	 *                                  TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DOUBLE.
+	 */
+	public Attribute(BlackboardAttribute.Type attributeType, double valueDouble) throws IllegalArgumentException {
+		super(attributeType, valueDouble);
+	}
+
+ 
+	/**
+	 * Constructs an attribute with a string value. The attribute should be
+	 * added to an appropriate artifact.
+	 *
+	 * @param attributeType The attribute type.
+	 * @param valueString   The attribute value.
+	 *
+	 * @throws IllegalArgumentException If the value type of the specified
+	 *                                  attribute type is not
+	 *                                  TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING.
+	 */
+	public Attribute(BlackboardAttribute.Type attributeType, String valueString) throws IllegalArgumentException {
+		super(attributeType, valueString);
+	}
+
+
+	/**
+	 * Constructs an attribute with a byte array value. The attribute should be
+	 * added to an appropriate artifact.
+	 *
+	 * @param attributeType The attribute type.
+	 * @param valueBytes    The attribute value.
+	 *
+	 * @throws IllegalArgumentException If the value type of the specified
+	 *                                  attribute type is not
+	 *                                  TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.BYTE.
+	 */
+	public Attribute(BlackboardAttribute.Type attributeType, byte[] valueBytes) throws IllegalArgumentException {
+		super(attributeType, valueBytes);
+	}
+	
+	/**
+	 * Constructs an artifact attribute. To be used when creating an attribute
+	 * based on a query of the blackboard _attributes table in the case
+	 * database.
+	 *
+	 * @param attributeOwnerId The owner id for this attribute.
+	 * @param attributeTypeID  The attribute type id.
+	 * @param valueType        The attribute value type.
+	 * @param valueInt         The value from the the value_int32 column.
+	 * @param valueLong        The value from the the value_int64 column.
+	 * @param valueDouble      The value from the the value_double column.
+	 * @param valueString      The value from the the value_text column.
+	 * @param valueBytes       The value from the the value_byte column.
+	 * @param sleuthkitCase    A reference to the SleuthkitCase object
+	 *                         representing the case database.
+	 */
+	Attribute(long attributeOwnerId, BlackboardAttribute.Type attributeType,  
+			int valueInt, long valueLong, double valueDouble, String valueString, byte[] valueBytes,
+			SleuthkitCase sleuthkitCase) {
+		super(attributeOwnerId, attributeType, valueInt, valueLong, valueDouble, valueString, valueBytes, sleuthkitCase);
+	}
+
+	/**
+	 * Gets the owner Id of this attribute. An owner is defined as the Object
+	 * to which this attribute is associated with.
+	 * Eg: For a file Attribute, the owner id would be the file object id.
+	 *
+	 * @return
+	 */
+	@Override
+	public long getAttributeOwnerId() {
+		return super.getAttributeOwnerId();
+	}
+
+
+	@Override
+	public int hashCode() {
+		return Objects.hash(
+				this.getAttributeType(), this.getValueInt(), this.getValueLong(), this.getValueDouble(),
+				this.getValueString(), this.getValueBytes());
+	}
+
+	@Override
+	public boolean equals(Object that) {
+		if (this == that) {
+			return true;
+		} else if (that instanceof Attribute) {
+ 			return isAttributeEquals(that);
+		} else {
+			return false;
+		}
+	}
+
+	@Override
+	public String toString() {
+		return MoreObjects.toStringHelper(this)
+				.add("attributeType", getAttributeType().toString())
+				.add("valueInt", getValueInt())
+				.add("valueLong", getValueLong())
+				.add("valueDouble", getValueDouble())
+				.add("valueString", getValueString())
+				.add("valueBytes", Arrays.toString(getValueBytes()) )
+				.add("Case", getCaseDatabase())
+				.toString();
+	}
+}
\ No newline at end of file
diff --git a/bindings/java/src/org/sleuthkit/datamodel/Blackboard.java b/bindings/java/src/org/sleuthkit/datamodel/Blackboard.java
index 389e7c6fb28436c04ba1263996f16768f39cbc96..4bc7acb92738dcb66e4904b6cd11c49ff12605a4 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/Blackboard.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/Blackboard.java
@@ -1,7 +1,7 @@
 /*
  * Sleuth Kit Data Model
  *
- * Copyright 2018 Basis Technology Corp.
+ * Copyright 2018-2020 Basis Technology Corp.
  * Contact: carrier <at> sleuthkit <dot> org
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -30,7 +30,11 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
+import java.util.logging.Level;
 import java.util.stream.Collectors;
+import java.util.logging.Logger;
+import org.sleuthkit.datamodel.SleuthkitCase.CaseDbConnection;
+import org.sleuthkit.datamodel.SleuthkitCase.CaseDbTransaction;
 
 /**
  * A representation of the blackboard, a place where artifacts and their
@@ -38,6 +42,8 @@
  */
 public final class Blackboard {
 
+	private static final Logger LOGGER = Logger.getLogger(Blackboard.class.getName());
+
 	private final SleuthkitCase caseDb;
 
 	/**
@@ -100,6 +106,8 @@ public void postArtifacts(Collection<BlackboardArtifact> artifacts, String modul
 	 * Gets an artifact type, creating it if it does not already exist. Use this
 	 * method to define custom artifact types.
 	 *
+	 * This assumes that the artifact type is of category EXTRACTED_DATA.
+	 *
 	 * @param typeName    The type name of the artifact type.
 	 * @param displayName The display name of the artifact type.
 	 *
@@ -110,8 +118,26 @@ public void postArtifacts(Collection<BlackboardArtifact> artifacts, String modul
 	 */
 	public BlackboardArtifact.Type getOrAddArtifactType(String typeName, String displayName) throws BlackboardException {
 
+		return getOrAddArtifactType(typeName, displayName, BlackboardArtifact.Category.EXTRACTED_DATA);
+	}
+
+	/**
+	 * Gets an artifact type, creating it if it does not already exist. Use this
+	 * method to define custom artifact types.
+	 *
+	 * @param typeName    The type name of the artifact type.
+	 * @param displayName The display name of the artifact type.
+	 * @param category    The artifact type category.
+	 *
+	 * @return A type object representing the artifact type.
+	 *
+	 * @throws BlackboardException If there is a problem getting or adding the
+	 *                             artifact type.
+	 */
+	public BlackboardArtifact.Type getOrAddArtifactType(String typeName, String displayName, BlackboardArtifact.Category category) throws BlackboardException {
+
 		try {
-			return caseDb.addBlackboardArtifactType(typeName, displayName);
+			return caseDb.addBlackboardArtifactType(typeName, displayName, category);
 		} catch (TskDataException typeExistsEx) {
 			try {
 				return caseDb.getArtifactType(typeName);
@@ -122,6 +148,204 @@ public BlackboardArtifact.Type getOrAddArtifactType(String typeName, String disp
 			throw new BlackboardException("Failed to get or add artifact type", ex);
 		}
 	}
+	
+	/**
+	 * Adds new analysis result artifact.
+	 *
+	 * @param artifactType    Type of analysis result artifact to create.
+	 * @param objId           Object id of parent.
+	 * @param dataSourceObjId Data source object id.
+	 * @param score	          Score associated with this analysis result.
+	 * @param conclusion      Conclusion of the analysis, may be null or an
+	 *                        empty string.
+	 * @param configuration   Configuration associated with this analysis, may
+	 *                        be null or an empty string.
+	 * @param justification   Justification, may be null or an empty string.
+	 * @param attributesList  Attributes to be attached to this analysis result
+	 *                        artifact.
+	 * @param transaction     DB transaction to use.
+	 *
+	* @return AnalysisResultAdded The analysis return added and the
+         current aggregate score of content.
+	 *
+	 * @throws BlackboardException exception thrown if a critical error occurs
+	 *                             within TSK core
+	 */
+	public AnalysisResultAdded newAnalysisResult(BlackboardArtifact.Type artifactType, long objId, long dataSourceObjId, Score score, String conclusion, String configuration, String justification, Collection<BlackboardAttribute> attributesList, CaseDbTransaction transaction) throws BlackboardException {
+		
+		try {
+			// add analysis result
+			AnalysisResult analysisResult =  caseDb.newAnalysisResult(artifactType, objId, dataSourceObjId, score, conclusion, configuration, justification, transaction.getConnection());
+			
+			// add the given attributes
+			if (attributesList != null && !attributesList.isEmpty()) {
+				analysisResult.addAttributes(attributesList, transaction);
+			}
+			
+			// update the final score for the object 
+			Score aggregateScore = caseDb.getScoringManager().updateAggregateScore(objId, dataSourceObjId, analysisResult.getScore(), transaction);
+			
+			// return the analysis result and the current aggregate score.
+			return new AnalysisResultAdded(analysisResult, aggregateScore);
+			
+		}  catch (TskCoreException ex) {
+			throw new BlackboardException("Failed to add analysis result.", ex);
+		}
+	}
+	
+	private final static String ANALYSIS_RESULT_QUERY_STRING = "SELECT DISTINCT arts.artifact_id AS artifact_id, " //NON-NLS
+				+ "arts.obj_id AS obj_id, arts.artifact_obj_id AS artifact_obj_id, arts.data_source_obj_id AS data_source_obj_id, arts.artifact_type_id AS artifact_type_id, "
+				+ " types.type_name AS type_name, types.display_name AS display_name, types.category_type as category_type,"//NON-NLS
+				+ " arts.review_status_id AS review_status_id, " //NON-NLS
+				+ " results.conclusion AS conclusion,  results.significance AS significance,  results.confidence AS confidence,  "
+				+ " results.configuration AS configuration,  results.justification AS justification "
+				+ " FROM blackboard_artifacts AS arts, tsk_analysis_results AS results, blackboard_artifact_types AS types " //NON-NLS
+				+ " WHERE arts.artifact_obj_id = results.obj_id " //NON-NLS
+				+ " AND arts.artifact_type_id = types.artifact_type_id"
+				+ " AND arts.review_status_id !=" + BlackboardArtifact.ReviewStatus.REJECTED.getID();
+
+	/**
+	 * Get all analysis results for a given object.
+	 *
+	 * @param objId Object id.
+	
+	 * @return list of analysis results.
+	 *
+	 * @throws TskCoreException exception thrown if a critical error occurs
+	 *                          within TSK core.
+	 */
+	public List<AnalysisResult> getAnalysisResults(long objId) throws TskCoreException {
+		return getAnalysisResultsWhere(" arts.obj_id = " + objId);
+	}
+	
+	/**
+	 * Get analysis results of the given type, for the given object.
+	 *
+	 * @param objId Object id.
+	 * @param artifactTypeId Result type to get.
+	
+	 * @return list of analysis results.
+	 *
+	 * @throws TskCoreException exception thrown if a critical error occurs
+	 *                          within TSK core.
+	 */
+	public List<AnalysisResult> getAnalysisResults(long objId, int artifactTypeId) throws TskCoreException {
+		
+		BlackboardArtifact.Type artifactType = new BlackboardArtifact.Type(BlackboardArtifact.ARTIFACT_TYPE.fromID(artifactTypeId));
+		if (artifactType.getCategory() != BlackboardArtifact.Category.ANALYSIS_RESULT) {
+			throw new TskCoreException(String.format("Artifact type id %d is not in analysis result catgeory.", artifactTypeId));
+		}
+		
+		String whereClause = " types.artifact_type_id = " + artifactTypeId
+							 + " AND arts.obj_id = " + objId; 
+		
+		return getAnalysisResultsWhere(whereClause);
+	}
+	
+	/**
+	 * Get all analysis results of a given type, for a given data source.
+	 *
+	 * @param dataSourceObjId Data source to look under.
+	 * @param artifactTypeId  Type of results to get.
+	 *
+	 * @return list of analysis results.
+	 *
+	 * @throws TskCoreException exception thrown if a critical error occurs
+	 *                          within TSK core.
+	 */
+
+// To keep the public API footprint minimal and necessary, this API is commented out 
+// till Autopsy implements and intgerates the concept of AnalysisResults. 
+// At that time, this api could be uncommented or deleted if it is not needed.
+	
+//	public List<AnalysisResult> getAnalysisResultsForDataSource(long dataSourceObjId, int artifactTypeId) throws TskCoreException {
+//		
+//		BlackboardArtifact.Type artifactType = new BlackboardArtifact.Type(BlackboardArtifact.ARTIFACT_TYPE.fromID(artifactTypeId));
+//		if (artifactType.getCategory() != BlackboardArtifact.Category.ANALYSIS_RESULT) {
+//			throw new TskCoreException(String.format("Artifact type id %d is not in analysis result catgeory.", artifactTypeId));
+//		}
+//
+//		String whereClause = " types.artifact_type_id = " + artifactTypeId
+//				+ " AND arts.data_source_obj_id = " + dataSourceObjId;  // NON-NLS
+//		
+//		return getAnalysisResultsWhere(whereClause);
+//	}
+	
+	/**
+	 * Get all analysis results matching the given where sub-clause.
+	 * 
+	 *
+	 * @param whereClause Where sub clause, specifies conditions to match.  
+	 * @return list of analysis results.
+	 *
+	 * @throws TskCoreException exception thrown if a critical error occurs
+	 *                          within TSK core.
+	 */
+	public List<AnalysisResult> getAnalysisResultsWhere(String whereClause) throws TskCoreException {
+		
+		try (CaseDbConnection connection = caseDb.getConnection()) {
+			return getAnalysisResultsWhere(whereClause, connection);
+		}
+	}
+	/**
+	 * Get all analysis results matching the given where sub-clause.
+	 * Uses the given database connection to execute the query.
+	 *
+	 * @param whereClause Where sub clause, specifies conditions to match.  
+	 * @param connection Database connection to use. 
+	 * 
+	 * @return list of analysis results.
+	 *
+	 * @throws TskCoreException exception thrown if a critical error occurs
+	 *                          within TSK core
+	 */
+	List<AnalysisResult> getAnalysisResultsWhere(String whereClause, CaseDbConnection connection) throws TskCoreException {
+
+		final String queryString = ANALYSIS_RESULT_QUERY_STRING
+				+ " AND " + whereClause;
+							
+		caseDb.acquireSingleUserCaseReadLock();
+		try (	Statement statement = connection.createStatement();
+				ResultSet resultSet = connection.executeQuery(statement, queryString);) {
+			
+			List<AnalysisResult> analysisResults = resultSetToAnalysisResults(resultSet);
+			return analysisResults;
+		} catch (SQLException ex) {
+			throw new TskCoreException(String.format("Error getting analysis results for WHERE caluse = '%s'", whereClause), ex);
+		} finally {
+			caseDb.releaseSingleUserCaseReadLock();
+		}
+	}
+	
+	/**
+	 * Creates AnalysisResult objects for the result set of a table query of the
+	 * form "SELECT * FROM blackboard_artifacts JOIN WHERE XYZ".
+	 *
+	 * @param rs A result set from a query of the blackboard_artifacts table of
+	 *           the form "SELECT * FROM blackboard_artifacts,
+	 *           tsk_analysis_results WHERE ...".
+	 *
+	 * @return A list of BlackboardArtifact objects.
+	 *
+	 * @throws SQLException     Thrown if there is a problem iterating through
+	 *                          the result set.
+	 * @throws TskCoreException Thrown if there is an error looking up the
+	 *                          artifact type id.
+	 */
+	private List<AnalysisResult> resultSetToAnalysisResults(ResultSet resultSet) throws SQLException, TskCoreException {
+		ArrayList<AnalysisResult> analysisResults = new ArrayList<>();
+
+		while (resultSet.next()) {
+			analysisResults.add(new AnalysisResult(caseDb, resultSet.getLong("artifact_id"), resultSet.getLong("obj_id"),
+					resultSet.getLong("artifact_obj_id"), resultSet.getLong("data_source_obj_id"),
+					resultSet.getInt("artifact_type_id"), resultSet.getString("type_name"), resultSet.getString("display_name"),
+					BlackboardArtifact.ReviewStatus.withID(resultSet.getInt("review_status_id")),
+					new Score(Score.Significance.fromID(resultSet.getInt("significance")), Score.Confidence.fromID(resultSet.getInt("confidence"))),
+					resultSet.getString("conclusion"), resultSet.getString("configuration"), resultSet.getString("justification")));
+		} //end for each resultSet
+
+		return analysisResults;
+	}
 
 	/**
 	 * Gets an attribute type, creating it if it does not already exist. Use
@@ -453,6 +677,66 @@ public static final class BlackboardException extends Exception {
 		}
 	}
 
+
+	/**
+	 * Add a new blackboard artifact with the given type.
+	 *
+	 * This api executes in the context of a transaction if one is provided.
+	 *
+	 * @param artifactType    The type of the artifact.
+	 * @param sourceObjId     The content that is the source of this artifact.
+	 * @param dataSourceObjId The data source the artifact source content
+	 *                        belongs to, may be the same as the sourceObjId.
+	 * @param attributes      The attributes. May be empty or null. 
+	 * @param transaction     The transaction in the scope of which the
+	 *                        operation is to be performed. Null may be
+	 *                        provided, if one is not available. 
+	 *
+	 * @return a new blackboard artifact
+	 *
+	 * @throws TskCoreException exception thrown if a critical error occurs
+	 *                          within tsk core
+	 */
+	public BlackboardArtifact newBlackboardArtifact(BlackboardArtifact.Type artifactType, long sourceObjId, long dataSourceObjId,
+			Collection<BlackboardAttribute> attributes, final CaseDbTransaction transaction) throws TskCoreException {
+
+	    CaseDbTransaction localTrans = null;
+		boolean isNewLocalTx = false;
+		if (transaction == null) {
+			localTrans = caseDb.beginTransaction();
+			isNewLocalTx = true;
+		}else {
+			localTrans = transaction;
+		}
+
+		try {
+			BlackboardArtifact blackboardArtifact = caseDb.newBlackboardArtifact(artifactType.getTypeID(), sourceObjId,
+					artifactType.getTypeName(), artifactType.getDisplayName(),
+					dataSourceObjId, localTrans.getConnection());
+
+			if (Objects.nonNull(attributes) && !attributes.isEmpty()) {
+				blackboardArtifact.addAttributes(attributes, localTrans);
+			}
+
+			if (isNewLocalTx) {
+				localTrans.commit();
+			}
+			return blackboardArtifact;
+
+		} catch (TskCoreException ex) {
+			try {
+				if (isNewLocalTx) {
+					localTrans.rollback();
+				}
+			} catch (TskCoreException ex2) {
+				LOGGER.log(Level.SEVERE, "Failed to rollback transaction after exception. "
+						+ "Error invoking newBlackboardArtifact with dataSourceObjId: " + dataSourceObjId + ",  sourceObjId: " + sourceObjId, ex2);
+			}
+			throw ex;
+		}
+	}
+
+
 	/**
 	 * Event published by SleuthkitCase when one or more artifacts are posted. A
 	 * posted artifact is complete (all attributes have been added) and ready
diff --git a/bindings/java/src/org/sleuthkit/datamodel/BlackboardArtifact.java b/bindings/java/src/org/sleuthkit/datamodel/BlackboardArtifact.java
index 591f0fae8fa3af11356098f12b57cf6314f1a4f1..4231c89537ddddd1982644761e523f2230ff25a6 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/BlackboardArtifact.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/BlackboardArtifact.java
@@ -20,6 +20,7 @@
 
 import java.io.Serializable;
 import java.io.UnsupportedEncodingException;
+import java.sql.SQLException;
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -30,8 +31,10 @@
 import java.util.Objects;
 import java.util.ResourceBundle;
 import java.util.Set;
+import org.sleuthkit.datamodel.Blackboard.BlackboardException;
 import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE;
 import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE;
+import org.sleuthkit.datamodel.SleuthkitCase.CaseDbTransaction;
 import org.sleuthkit.datamodel.SleuthkitCase.ObjectInfo;
 
 /**
@@ -193,6 +196,7 @@ public String getArtifactTypeName() {
 		return this.artifactTypeName;
 	}
 
+
 	/**
 	 * Gets the artifact type display name for this artifact.
 	 *
@@ -384,6 +388,43 @@ public void addAttributes(Collection<BlackboardAttribute> attributes) throws Tsk
 		attrsCache.addAll(attributes);
 	}
 
+	/**
+	 * Adds a collection of attributes to this artifact in a single operation
+	 * (faster than adding each attribute individually) within a transaction
+	 * supplied by the caller.
+	 *
+	 * @param attributes        The collection of attributes.
+	 * @param caseDbTransaction The transaction in the scope of which the
+	 *                          operation is to be performed, managed by the
+	 *                          caller. Null is not permitted.
+	 *
+	 * @throws TskCoreException         If an error occurs and the attributes
+	 *                                  were not added to the artifact.
+	 * @throws IllegalArgumentException If <code>caseDbTransaction</code> is
+	 *                                  null or if <code>attributes</code> is
+	 *                                  null or empty.
+	 */
+	public void addAttributes(Collection<BlackboardAttribute> attributes, final SleuthkitCase.CaseDbTransaction caseDbTransaction) throws TskCoreException {
+
+		if (Objects.isNull(attributes) || attributes.isEmpty()) {
+			throw new IllegalArgumentException("null or empty attributes passed to addAttributes");
+		}
+		if (Objects.isNull(caseDbTransaction) ) {
+			throw new IllegalArgumentException("null caseDbTransaction passed to addAttributes");
+		}
+		try {
+			for (final BlackboardAttribute attribute : attributes) {
+				attribute.setArtifactId(artifactId);
+				attribute.setCaseDatabase(getSleuthkitCase());
+				getSleuthkitCase().addBlackBoardAttribute(attribute, artifactTypeId, caseDbTransaction.getConnection());
+			}
+			attrsCache.addAll(attributes);
+		} catch (SQLException ex) {
+			throw new TskCoreException("Error adding blackboard attributes", ex);
+		}
+	}
+
+
 	/**
 	 * This overiding implementation returns the unique path of the parent. It
 	 * does not include the Artifact name in the unique path.
@@ -437,6 +478,23 @@ public ArrayList<BlackboardArtifact> getAllArtifacts() throws TskCoreException {
 		return new ArrayList<BlackboardArtifact>();
 	}
 
+	@Override
+	public List<AnalysisResult> getAllAnalysisResults() throws TskCoreException {
+		return sleuthkitCase.getBlackboard().getAnalysisResults(artifactObjId);
+	}
+	
+	@Override
+	public Score getAggregateScore() throws TskCoreException {
+		return sleuthkitCase.getScoringManager().getAggregateScore(artifactObjId);
+		
+	}
+	
+
+	@Override
+	public List<AnalysisResult> getAnalysisResults(BlackboardArtifact.Type artifactType) throws TskCoreException {
+		return sleuthkitCase.getBlackboard().getAnalysisResults(artifactObjId, artifactType.getTypeID()); //NON-NLS
+	}
+	
 	/**
 	 * Get all artifacts associated with this content that have the given type
 	 * name
@@ -627,6 +685,19 @@ public BlackboardArtifact newArtifact(int artifactTypeID) throws TskCoreExceptio
 		throw new TskCoreException("Cannot create artifact of an artifact. Not supported.");
 	}
 
+	@Override
+	public AnalysisResultAdded newAnalysisResult(BlackboardArtifact.Type artifactType, Score score, String conclusion, String configuration, String justification, Collection<BlackboardAttribute> attributesList) throws TskCoreException {
+		CaseDbTransaction trans = sleuthkitCase.beginTransaction();
+		try {
+			AnalysisResultAdded resultAdded = sleuthkitCase.getBlackboard().newAnalysisResult(artifactType, this.getObjectID(), this.getDataSource().getId(), score, conclusion, configuration, justification, attributesList, trans);
+
+			trans.commit();
+			return resultAdded;
+		} catch (BlackboardException ex) {
+			trans.rollback();
+			throw new TskCoreException("Error adding analysis result.", ex);
+		}
+	}
 	/**
 	 * Create and add an artifact associated with this content to the blackboard
 	 *
@@ -818,6 +889,8 @@ private void loadArtifactContent() throws TskCoreException {
 
 	}
 
+
+
 	/**
 	 * An artifact type.
 	 */
@@ -827,6 +900,7 @@ public static final class Type implements Serializable {
 		private final String typeName;
 		private final int typeID;
 		private final String displayName;
+		private final Category category;
 
 		/**
 		 * Constructs a custom artifact type.
@@ -836,9 +910,22 @@ public static final class Type implements Serializable {
 		 * @param displayName The display name of the type.
 		 */
 		public Type(int typeID, String typeName, String displayName) {
+			this(typeID, typeName, displayName, Category.EXTRACTED_DATA);
+		}
+
+		/**
+		 * Constructs a custom artifact type.
+		 *
+		 * @param typeName    The name of the type.
+		 * @param typeID      The id of the type.
+		 * @param displayName The display name of the type.
+		 * @param category    The artifact type category.
+		 */
+		public Type(int typeID, String typeName, String displayName, Category category) {
 			this.typeID = typeID;
 			this.typeName = typeName;
 			this.displayName = displayName;
+			this.category = category;
 		}
 
 		/**
@@ -847,7 +934,7 @@ public Type(int typeID, String typeName, String displayName) {
 		 * @param type An element of the ARTIFACT_TYPE enum.
 		 */
 		public Type(ARTIFACT_TYPE type) {
-			this(type.getTypeID(), type.getLabel(), type.getDisplayName());
+			this(type.getTypeID(), type.getLabel(), type.getDisplayName(), type.getCategory());
 		}
 
 		/**
@@ -877,6 +964,15 @@ public String getDisplayName() {
 			return this.displayName;
 		}
 
+		/**
+		 * Gets category of this artifact type.
+		 *
+		 * @return The artifact type category.
+		 */
+		public Category getCategory() {
+			return category;
+		}
+
 		/**
 		 * Tests this artifact type for equality with another object.
 		 *
@@ -985,12 +1081,12 @@ public enum ARTIFACT_TYPE implements SleuthkitVisitableItem {
 		 * A search hit for a keyword.
 		 */
 		TSK_KEYWORD_HIT(9, "TSK_KEYWORD_HIT",
-				bundle.getString("BlackboardArtifact.tskKeywordHits.text")),
+				bundle.getString("BlackboardArtifact.tskKeywordHits.text"), Category.ANALYSIS_RESULT),
 		/**
 		 * A hit for a hash set (hash database).
 		 */
 		TSK_HASHSET_HIT(10, "TSK_HASHSET_HIT", //NON-NLS
-				bundle.getString("BlackboardArtifact.tskHashsetHit.text")),
+				bundle.getString("BlackboardArtifact.tskHashsetHit.text"), Category.ANALYSIS_RESULT),
 		/**
 		 * An attached device.
 		 */
@@ -1001,7 +1097,7 @@ public enum ARTIFACT_TYPE implements SleuthkitVisitableItem {
 		 * interesting.
 		 */
 		TSK_INTERESTING_FILE_HIT(12, "TSK_INTERESTING_FILE_HIT", //NON-NLS
-				bundle.getString("BlackboardArtifact.tskInterestingFileHit.text")), ///< an interesting/notable file hit
+				bundle.getString("BlackboardArtifact.tskInterestingFileHit.text"), Category.ANALYSIS_RESULT), ///< an interesting/notable file hit
 		/**
 		 * An email message.
 		 */
@@ -1124,18 +1220,18 @@ public enum ARTIFACT_TYPE implements SleuthkitVisitableItem {
 		 * An encrypted file.
 		 */
 		TSK_ENCRYPTION_DETECTED(33, "TSK_ENCRYPTION_DETECTED", //NON-NLS
-				bundle.getString("BlackboardArtifact.tskEncryptionDetected.text")),
+				bundle.getString("BlackboardArtifact.tskEncryptionDetected.text"), Category.ANALYSIS_RESULT),
 		/**
 		 * A file with an extension that does not match its MIME type.
 		 */
 		TSK_EXT_MISMATCH_DETECTED(34, "TSK_EXT_MISMATCH_DETECTED", //NON-NLS
-				bundle.getString("BlackboardArtifact.tskExtMismatchDetected.text")),
+				bundle.getString("BlackboardArtifact.tskExtMismatchDetected.text"), Category.ANALYSIS_RESULT),
 		/**
 		 * An meta-artifact to call attention to an artifact deemed to be
 		 * interesting.
 		 */
 		TSK_INTERESTING_ARTIFACT_HIT(35, "TSK_INTERESTING_ARTIFACT_HIT", //NON-NLS
-				bundle.getString("BlackboardArtifact.tskInterestingArtifactHit.text")),
+				bundle.getString("BlackboardArtifact.tskInterestingArtifactHit.text"), Category.ANALYSIS_RESULT),
 		/**
 		 * A route based on GPS coordinates. Use
 		 * org.sleuthkit.datamodel.blackboardutils.GeoArtifactsHelper.addRoute()
@@ -1152,7 +1248,7 @@ public enum ARTIFACT_TYPE implements SleuthkitVisitableItem {
 		 * A human face was detected in a media file.
 		 */
 		TSK_FACE_DETECTED(38, "TSK_FACE_DETECTED", //NON-NLS
-				bundle.getString("BlackboardArtifact.tskFaceDetected.text")),
+				bundle.getString("BlackboardArtifact.tskFaceDetected.text"), Category.ANALYSIS_RESULT),
 		/**
 		 * An account.
 		 */
@@ -1162,12 +1258,12 @@ public enum ARTIFACT_TYPE implements SleuthkitVisitableItem {
 		 * An encrypted file.
 		 */
 		TSK_ENCRYPTION_SUSPECTED(40, "TSK_ENCRYPTION_SUSPECTED", //NON-NLS
-				bundle.getString("BlackboardArtifact.tskEncryptionSuspected.text")),
+				bundle.getString("BlackboardArtifact.tskEncryptionSuspected.text"), Category.ANALYSIS_RESULT),
 		/*
 		 * A classifier detected an object in a media file.
 		 */
 		TSK_OBJECT_DETECTED(41, "TSK_OBJECT_DETECTED", //NON-NLS
-				bundle.getString("BlackboardArtifact.tskObjectDetected.text")),
+				bundle.getString("BlackboardArtifact.tskObjectDetected.text"), Category.ANALYSIS_RESULT),
 		/**
 		 * A wireless network.
 		 */
@@ -1317,6 +1413,7 @@ public enum ARTIFACT_TYPE implements SleuthkitVisitableItem {
 		private final String label;
 		private final int typeId;
 		private final String displayName;
+		private final Category category;
 
 		/**
 		 * Constructs a value for the standard artifact types enum.
@@ -1326,9 +1423,22 @@ public enum ARTIFACT_TYPE implements SleuthkitVisitableItem {
 		 * @param displayName The type display name.
 		 */
 		private ARTIFACT_TYPE(int typeId, String label, String displayName) {
+			this(typeId, label, displayName, Category.EXTRACTED_DATA);
+		}
+
+		/**
+		 * Constructs a value for the standard artifact types enum.
+		 *
+		 * @param typeId      The type id.
+		 * @param label       The type name.
+		 * @param displayName The type display name.
+		 * @param category	  The type category.
+		 */
+		private ARTIFACT_TYPE(int typeId, String label, String displayName, Category category) {
 			this.typeId = typeId;
 			this.label = label;
 			this.displayName = displayName;
+			this.category = category;
 		}
 
 		/**
@@ -1349,6 +1459,15 @@ public String getLabel() {
 			return this.label;
 		}
 
+		/**
+		 * Gets the type category for this standard artifact type.
+		 *
+		 * @return The type category.
+		 */
+		public Category getCategory() {
+			return this.category;
+		}
+
 		/**
 		 * Gets the standard artifact type enum value that corresponds to a
 		 * given type name (label).
@@ -1412,6 +1531,82 @@ public <T> T accept(SleuthkitItemVisitor<T> visitor) {
 
 	}
 
+	/**
+	 * Enumeration to encapsulate categories of artifact.
+	 *
+	 * Some artifact types represent data directly extracted from a data source, 
+	 * while others may be the result of some analysis done on the extracted 
+	 * data.
+	 */
+	public enum Category {
+
+		EXTRACTED_DATA(0, "EXTRACTED_DATA", ResourceBundle.getBundle("org.sleuthkit.datamodel.Bundle").getString("CategoryType.ExtractedData")), // artifact is data that is directly/indirectly extracted from a data source.
+		ANALYSIS_RESULT(1, "ANALYSIS_RESULT", ResourceBundle.getBundle("org.sleuthkit.datamodel.Bundle").getString("CategoryType.AnalysisResult")); // artifacts represents outcome of analysis of data.
+
+		private final Integer id;
+		private final String name;
+		private final String displayName;
+
+		private final static Map<Integer, Category> idToCategory = new HashMap<Integer, Category>();
+
+		static {
+			for (Category status : values()) {
+				idToCategory.put(status.getID(), status);
+			}
+		}
+
+		/**
+		 * Constructs a value for the category enum.
+		 *
+		 * @param id             The category id.
+		 * @param name           The category name
+		 * @param displayNameKey Category display name.
+		 */
+		private Category(Integer id, String name, String displayName) {
+			this.id = id;
+			this.name = name;
+			this.displayName = displayName;
+		}
+
+		/**
+		 * Gets the category value with the given id, if one exists.
+		 *
+		 * @param id A category id.
+		 *
+		 * @return The category with the given id, or null if none exists.
+		 */
+		public static Category fromID(int id) {
+			return idToCategory.get(id);
+		}
+
+		/**
+		 * Gets the id of this review status.
+		 *
+		 * @return The id of this review status.
+		 */
+		public Integer getID() {
+			return id;
+		}
+
+		/**
+		 * Gets the name of this category.
+		 *
+		 * @return The name of this category.
+		 */
+		String getName() {
+			return name;
+		}
+
+		/**
+		 * Gets the display name of this category.
+		 *
+		 * @return The display name of this category.
+		 */
+		public String getDisplayName() {
+			return displayName;
+		}
+	}
+
 	/**
 	 * Enum to represent the review status of an artifact.
 	 */
diff --git a/bindings/java/src/org/sleuthkit/datamodel/BlackboardAttribute.java b/bindings/java/src/org/sleuthkit/datamodel/BlackboardAttribute.java
index dbe1b340d8d9d7cb3121ea715cd85af85892603d..3b5f1658062aac227475dadfd8fca345f977aa12 100755
--- a/bindings/java/src/org/sleuthkit/datamodel/BlackboardAttribute.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/BlackboardAttribute.java
@@ -42,28 +42,20 @@
  * attribute by calling the appropriate BlackboardAttribute constructor. It can
  * also be used to do blackboard queries involving the custom type.
  */
-public class BlackboardAttribute {
+public class BlackboardAttribute extends AbstractAttribute{
 
-	private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
 	private static final Logger LOGGER = Logger.getLogger(BlackboardAttribute.class.getName());
 
 	private static final ResourceBundle bundle = ResourceBundle.getBundle("org.sleuthkit.datamodel.Bundle");
-	private BlackboardAttribute.Type attributeType;
-	private final int valueInt;
-	private final long valueLong;
-	private final double valueDouble;
-	private final String valueString;
-	private final byte[] valueBytes;
+
 	private String context;
-	private long artifactID;
-	private SleuthkitCase sleuthkitCase;
 	private String sources;
 	
 	// Cached parent artifact. This field is populated lazily upon the first
 	// call to getParentArtifact().
 	private BlackboardArtifact parentArtifact;
-	
-	// The parent data source is defined as being 
+
+	// The parent data source is defined as being
 	// the data source of the parent artifact.
 	private Long parentDataSourceID;
 
@@ -80,17 +72,8 @@ public class BlackboardAttribute {
 	 *                                  TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.INTEGER.
 	 */
 	public BlackboardAttribute(ATTRIBUTE_TYPE attributeType, String source, int valueInt) throws IllegalArgumentException {
-		if (attributeType.getValueType() != TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.INTEGER) {
-			throw new IllegalArgumentException("Value types do not match");
-		}
-		this.artifactID = 0;
-		this.attributeType = new BlackboardAttribute.Type(attributeType);
+		super(new BlackboardAttribute.Type(attributeType), valueInt);
 		this.sources = replaceNulls(source);
-		this.valueInt = valueInt;
-		this.valueLong = 0;
-		this.valueDouble = 0;
-		this.valueString = "";
-		this.valueBytes = new byte[0];
 		this.context = "";
 	}
 
@@ -107,17 +90,8 @@ public BlackboardAttribute(ATTRIBUTE_TYPE attributeType, String source, int valu
 	 *                                  TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.INTEGER.
 	 */
 	public BlackboardAttribute(Type attributeType, String source, int valueInt) throws IllegalArgumentException {
-		if (attributeType.getValueType() != TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.INTEGER) {
-			throw new IllegalArgumentException("Type mismatched with value type");
-		}
-		this.artifactID = 0;
-		this.attributeType = attributeType;
+		super(attributeType, valueInt);
 		this.sources = replaceNulls(source);
-		this.valueInt = valueInt;
-		this.valueLong = 0;
-		this.valueDouble = 0;
-		this.valueString = "";
-		this.valueBytes = new byte[0];
 		this.context = "";
 	}
 
@@ -137,18 +111,8 @@ public BlackboardAttribute(Type attributeType, String source, int valueInt) thro
 	 *                                  TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DATETIME.
 	 */
 	public BlackboardAttribute(ATTRIBUTE_TYPE attributeType, String source, long valueLong) throws IllegalArgumentException {
-		if (attributeType.getValueType() != TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.LONG
-				&& attributeType.getValueType() != TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DATETIME) {
-			throw new IllegalArgumentException("Value types do not match");
-		}
-		this.artifactID = 0;
-		this.attributeType = new BlackboardAttribute.Type(attributeType);
+		super(new BlackboardAttribute.Type(attributeType), valueLong);
 		this.sources = replaceNulls(source);
-		this.valueInt = 0;
-		this.valueLong = valueLong;
-		this.valueDouble = 0;
-		this.valueString = "";
-		this.valueBytes = new byte[0];
 		this.context = "";
 	}
 
@@ -167,18 +131,8 @@ public BlackboardAttribute(ATTRIBUTE_TYPE attributeType, String source, long val
 	 *                                  TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DATETIME.
 	 */
 	public BlackboardAttribute(Type attributeType, String source, long valueLong) throws IllegalArgumentException {
-		if (attributeType.getValueType() != TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.LONG
-				&& attributeType.getValueType() != TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DATETIME) {
-			throw new IllegalArgumentException("Type mismatched with value type");
-		}
-		this.artifactID = 0;
-		this.attributeType = attributeType;
+		super(attributeType, valueLong);
 		this.sources = replaceNulls(source);
-		this.valueInt = 0;
-		this.valueLong = valueLong;
-		this.valueDouble = 0;
-		this.valueString = "";
-		this.valueBytes = new byte[0];
 		this.context = "";
 	}
 
@@ -195,19 +149,9 @@ public BlackboardAttribute(Type attributeType, String source, long valueLong) th
 	 *                                  TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DOUBLE.
 	 */
 	public BlackboardAttribute(ATTRIBUTE_TYPE attributeType, String source, double valueDouble) throws IllegalArgumentException {
-		if (attributeType.getValueType() != TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DOUBLE) {
-			throw new IllegalArgumentException("Value types do not match");
-		}
-		this.artifactID = 0;
-		this.attributeType = new BlackboardAttribute.Type(attributeType);
+		super(new BlackboardAttribute.Type(attributeType), valueDouble);
 		this.sources = replaceNulls(source);
-		this.valueInt = 0;
-		this.valueLong = 0;
-		this.valueDouble = valueDouble;
-		this.valueString = "";
-		this.valueBytes = new byte[0];
 		this.context = "";
-
 	}
 
 	/**
@@ -223,17 +167,8 @@ public BlackboardAttribute(ATTRIBUTE_TYPE attributeType, String source, double v
 	 *                                  TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DOUBLE.
 	 */
 	public BlackboardAttribute(Type attributeType, String source, double valueDouble) throws IllegalArgumentException {
-		if (attributeType.getValueType() != TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DOUBLE) {
-			throw new IllegalArgumentException("Type mismatched with value type");
-		}
-		this.artifactID = 0;
-		this.attributeType = attributeType;
+		super(attributeType, valueDouble);
 		this.sources = replaceNulls(source);
-		this.valueInt = 0;
-		this.valueLong = 0;
-		this.valueDouble = valueDouble;
-		this.valueString = "";
-		this.valueBytes = new byte[0];
 		this.context = "";
 	}
 
@@ -252,22 +187,8 @@ public BlackboardAttribute(Type attributeType, String source, double valueDouble
 	 *                                  TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON
 	 */
 	public BlackboardAttribute(ATTRIBUTE_TYPE attributeType, String source, String valueString) throws IllegalArgumentException {
-		if (attributeType.getValueType() != TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING
-				&& attributeType.getValueType() != TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON) {
-			throw new IllegalArgumentException("Value types do not match");
-		}
-		this.artifactID = 0;
-		this.attributeType = new BlackboardAttribute.Type(attributeType);
+		super(new BlackboardAttribute.Type(attributeType), valueString);
 		this.sources = replaceNulls(source);
-		this.valueInt = 0;
-		this.valueLong = 0;
-		this.valueDouble = 0;
-		if (valueString == null) {
-			this.valueString = "";
-		} else {
-			this.valueString = replaceNulls(valueString).trim();
-		}
-		this.valueBytes = new byte[0];
 		this.context = "";
 	}
 
@@ -284,22 +205,8 @@ public BlackboardAttribute(ATTRIBUTE_TYPE attributeType, String source, String v
 	 *                                  TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING.
 	 */
 	public BlackboardAttribute(Type attributeType, String source, String valueString) throws IllegalArgumentException {
-		if (attributeType.getValueType() != TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING
-				&& attributeType.getValueType() != TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON) {
-			throw new IllegalArgumentException("Type mismatched with value type");
-		}
-		this.artifactID = 0;
-		this.attributeType = attributeType;
+		super(attributeType, valueString);
 		this.sources = replaceNulls(source);
-		this.valueInt = 0;
-		this.valueLong = 0;
-		this.valueDouble = 0;
-		if (valueString == null) {
-			this.valueString = "";
-		} else {
-			this.valueString = replaceNulls(valueString).trim();
-		}
-		this.valueBytes = new byte[0];
 		this.context = "";
 	}
 
@@ -316,22 +223,9 @@ public BlackboardAttribute(Type attributeType, String source, String valueString
 	 *                                  TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.BYTE.
 	 */
 	public BlackboardAttribute(ATTRIBUTE_TYPE attributeType, String source, byte[] valueBytes) throws IllegalArgumentException {
-		if (attributeType.getValueType() != TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.BYTE) {
-			throw new IllegalArgumentException("Value types do not match");
-		}
-		this.artifactID = 0;
-		this.attributeType = new BlackboardAttribute.Type(attributeType);
+		super(new BlackboardAttribute.Type(attributeType), valueBytes);
 		this.sources = replaceNulls(source);
 		this.context = "";
-		this.valueInt = 0;
-		this.valueLong = 0;
-		this.valueDouble = 0;
-		this.valueString = "";
-		if (valueBytes == null) {
-			this.valueBytes = new byte[0];
-		} else {
-			this.valueBytes = valueBytes;
-		}
 	}
 
 	/**
@@ -347,22 +241,9 @@ public BlackboardAttribute(ATTRIBUTE_TYPE attributeType, String source, byte[] v
 	 *                                  TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.BYTE.
 	 */
 	public BlackboardAttribute(Type attributeType, String source, byte[] valueBytes) throws IllegalArgumentException {
-		if (attributeType.getValueType() != TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.BYTE) {
-			throw new IllegalArgumentException("Type mismatched with value type");
-		}
-		this.artifactID = 0;
-		this.attributeType = attributeType;
+		super(attributeType, valueBytes);
 		this.sources = replaceNulls(source);
 		this.context = "";
-		this.valueInt = 0;
-		this.valueLong = 0;
-		this.valueDouble = 0;
-		this.valueString = "";
-		if (valueBytes == null) {
-			this.valueBytes = new byte[0];
-		} else {
-			this.valueBytes = valueBytes;
-		}
 	}
 
 	/**
@@ -373,76 +254,16 @@ public BlackboardAttribute(Type attributeType, String source, byte[] valueBytes)
 	 * @return The artifact id or zero if the artifact id has not been set.
 	 */
 	public long getArtifactID() {
-		return artifactID;
+		return super.getAttributeOwnerId();
 	}
 
 	/**
-	 * Gets the type of this attribute.
-	 *
-	 * @return The attribute type.
-	 */
-	public BlackboardAttribute.Type getAttributeType() {
-		return this.attributeType;
-	}
-
-	/**
-	 * Gets the value type.
-	 *
-	 * @return The value type
-	 */
-	public TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE getValueType() {
-		return attributeType.getValueType();
-	}
-
-	/**
-	 * Gets the value of this attribute. The value is only valid if the
-	 * attribute value type is TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.INTEGER.
-	 *
-	 * @return The attribute value.
-	 */
-	public int getValueInt() {
-		return valueInt;
-	}
-
-	/**
-	 * Gets the value of this attribute. The value is only valid if the
-	 * attribute value type is TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.LONG.
-	 *
-	 * @return The attribute value.
-	 */
-	public long getValueLong() {
-		return valueLong;
-	}
-
-	/**
-	 * Gets the value of this attribute. The value is only valid if the
-	 * attribute value type is TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DOUBLE.
-	 *
-	 * @return The attribute value.
-	 */
-	public double getValueDouble() {
-		return valueDouble;
-	}
-
-	/**
-	 * Gets the value of this attribute. The value is only valid if the
-	 * attribute value type is TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING or
-	 * TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON.
-	 *
-	 * @return The attribute value.
-	 */
-	public String getValueString() {
-		return valueString;
-	}
-
-	/**
-	 * Gets the value of this attribute. The value is only valid if the
-	 * attribute value type is TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.BYTE.
+	 * Sets the artifact id.
 	 *
-	 * @return The attribute value.
+	 * @param artifactID The artifact id.
 	 */
-	public byte[] getValueBytes() {
-		return Arrays.copyOf(valueBytes, valueBytes.length);
+	void setArtifactId(long artifactID) {
+		super.setAttributeOwnerId(artifactID);
 	}
 
 	/**
@@ -467,7 +288,7 @@ public List<String> getSources() {
 	 * @throws org.sleuthkit.datamodel.TskCoreException
 	 */
 	public void addSource(String source) throws TskCoreException {
-		this.sources = sleuthkitCase.addSourceToArtifactAttribute(this, source);
+		this.sources = getCaseDatabase().addSourceToArtifactAttribute(this, source);
 	}
 
 	/**
@@ -483,33 +304,36 @@ public void addSource(String source) throws TskCoreException {
 	 */
 	public BlackboardArtifact getParentArtifact() throws TskCoreException {
 		if (parentArtifact == null) {
-			parentArtifact = sleuthkitCase.getBlackboardArtifact(artifactID);
+			parentArtifact = getCaseDatabase().getBlackboardArtifact(getArtifactID());
 		}
 		return parentArtifact;
 	}
 
+
 	@Override
 	public int hashCode() {
-		int hash = 5;
-		hash = 97 * hash + (int) (this.artifactID ^ (this.artifactID >>> 32));
-		return hash;
+		return Objects.hash(
+				this.getAttributeType(), this.getValueInt(), this.getValueLong(), this.getValueDouble(),
+				this.getValueString(), this.getValueBytes(), this.getSources(), getContext());
 	}
 
 	@Override
-	public boolean equals(Object obj) {
-		if (obj == null) {
-			return false;
-		}
-		if (getClass() != obj.getClass()) {
+	public boolean equals(Object that) {
+		if (this == that) {
+			return true;
+		} else if (that instanceof BlackboardAttribute) {
+			BlackboardAttribute other = (BlackboardAttribute) that;
+			Object[] thisObject = new Object[]{this.getSources(), this.getContext()};
+			Object[] otherObject = new Object[]{other.getSources(), other.getContext()};
+			return isAttributeEquals(that) && Objects.deepEquals(thisObject, otherObject);
+		} else {
 			return false;
 		}
-		final BlackboardAttribute other = (BlackboardAttribute) obj;
-		return this.artifactID == other.getArtifactID();
 	}
 
 	@Override
 	public String toString() {
-		return "BlackboardAttribute{" + "artifactID=" + artifactID + ", attributeType=" + attributeType.toString() + ", moduleName=" + sources + ", context=" + context + ", valueInt=" + valueInt + ", valueLong=" + valueLong + ", valueDouble=" + valueDouble + ", valueString=" + valueString + ", valueBytes=" + Arrays.toString(valueBytes) + ", Case=" + sleuthkitCase + '}'; //NON-NLS
+		return "BlackboardAttribute{" + "artifactID=" + getArtifactID() + ", attributeType=" + getAttributeType().toString() + ", moduleName=" + getSources() + ", context=" + context + ", valueInt=" + getValueInt() + ", valueLong=" + getValueLong() + ", valueDouble=" + getValueDouble() + ", valueString=" + getValueString() + ", valueBytes=" + Arrays.toString(getValueBytes()) + ", Case=" + getCaseDatabase() + '}'; //NON-NLS
 	}
 
 	/**
@@ -518,35 +342,14 @@ public String toString() {
 	 * @return The value as a string.
 	 */
 	public String getDisplayString() {
-		switch (attributeType.getValueType()) {
-			case STRING:
-				return getValueString();
-			case INTEGER:
-				if (attributeType.getTypeID() == ATTRIBUTE_TYPE.TSK_READ_STATUS.getTypeID()) {
-					if (getValueInt() == 0) {
-						return "Unread";
-					} else {
-						return "Read";
-					}
-				}
-				return Integer.toString(getValueInt());
-			case LONG:
-				// SHOULD at some point figure out how to convert times in here 
-				// based on preferred formats and such.  Perhaps provide another 
-				// method that takes a formatter argument. 
-				return Long.toString(getValueLong());
-			case DOUBLE:
-				return Double.toString(getValueDouble());
-			case BYTE:
-				return bytesToHexString(getValueBytes());
-
+		switch (getAttributeType().getValueType()) {
 			case DATETIME: {
 				try {
 					if (parentDataSourceID == null) {
 						BlackboardArtifact parent = getParentArtifact();
 						parentDataSourceID = parent.getDataSourceObjectID();
 					}
-					final Content dataSource = sleuthkitCase.getContentById(parentDataSourceID);
+					final Content dataSource = getCaseDatabase().getContentById(parentDataSourceID);
 					if ((dataSource != null) && (dataSource instanceof Image)) {
 						// return the date/time string in the timezone associated with the datasource,
 						Image image = (Image) dataSource;
@@ -559,11 +362,10 @@ public String getDisplayString() {
 				// return time string in default timezone
 				return TimeUtilities.epochToTime(getValueLong());
 			}
-			case JSON: {
-				return getValueString();
+			default:{
+				return super.getDisplayString();
 			}
 		}
-		return "";
 	}
 
 	/**
@@ -588,49 +390,15 @@ public String getDisplayString() {
 			int valueInt, long valueLong, double valueDouble, String valueString, byte[] valueBytes,
 			SleuthkitCase sleuthkitCase) {
 
-		this.artifactID = artifactID;
-		this.attributeType = attributeType;
+		super(artifactID, attributeType, valueInt, valueLong, valueDouble, valueString, valueBytes, sleuthkitCase);
 		this.sources = replaceNulls(source);
 		this.context = replaceNulls(context);
-		this.valueInt = valueInt;
-		this.valueLong = valueLong;
-		this.valueDouble = valueDouble;
-		if (valueString == null) {
-			this.valueString = "";
-		} else {
-			this.valueString = replaceNulls(valueString).trim();
-		}
-		if (valueBytes == null) {
-			this.valueBytes = new byte[0];
-		} else {
-			this.valueBytes = valueBytes;
-		}
-		this.sleuthkitCase = sleuthkitCase;
-	}
-
-	/**
-	 * Sets the reference to the SleuthkitCase object that represents the case
-	 * database.
-	 *
-	 * @param sleuthkitCase A reference to a SleuthkitCase object.
-	 */
-	void setCaseDatabase(SleuthkitCase sleuthkitCase) {
-		this.sleuthkitCase = sleuthkitCase;
 	}
 
-	/**
-	 * Sets the artifact id.
-	 *
-	 * @param artifactID The artifact id.
-	 */
-	void setArtifactId(long artifactID) {
-		this.artifactID = artifactID;
-	}
-	
 	/**
 	 * Sets the parent data source id. The parent data source is defined
 	 * as being the data source of the parent artifact.
-	 * 
+	 *
 	 * @param parentDataSourceID The parent data source id.
 	 */
 	void setParentDataSourceID(long parentDataSourceID) {
@@ -649,35 +417,6 @@ String getSourcesCSV() {
 		return sources;
 	}
 
-	/**
-	 * Converts a byte array to a string.
-	 *
-	 * @param bytes The byte array.
-	 *
-	 * @return The string.
-	 */
-	static String bytesToHexString(byte[] bytes) {
-		// from http://stackoverflow.com/questions/9655181/convert-from-byte-array-to-hex-string-in-java
-		char[] hexChars = new char[bytes.length * 2];
-		for (int j = 0; j < bytes.length; j++) {
-			int v = bytes[j] & 0xFF;
-			hexChars[j * 2] = HEX_ARRAY[v >>> 4];
-			hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
-		}
-		return new String(hexChars);
-	}
-
-	/**
-	 * Replace all NUL characters in the string with the SUB character
-	 *
-	 * @param text The input string.
-	 *
-	 * @return The output string.
-	 */
-	private String replaceNulls(String text) {
-		return text.replace((char) 0x00, (char) 0x1A);
-	}
-
 	/**
 	 * Represents the type of an attribute.
 	 */
@@ -1845,7 +1584,7 @@ String getContextString() {
 	 */
 	@Deprecated
 	public int getAttributeTypeID() {
-		return attributeType.getTypeID();
+		return getAttributeType().getTypeID();
 	}
 
 	/**
@@ -1859,7 +1598,7 @@ public int getAttributeTypeID() {
 	 */
 	@Deprecated
 	public String getAttributeTypeName() throws TskCoreException {
-		return attributeType.getTypeName();
+		return getAttributeType().getTypeName();
 	}
 
 	/**
@@ -1874,7 +1613,7 @@ public String getAttributeTypeName() throws TskCoreException {
 	 */
 	@Deprecated
 	public String getAttributeTypeDisplayName() throws TskCoreException {
-		return attributeType.getDisplayName();
+		return getAttributeType().getDisplayName();
 	}
 
 	/**
@@ -1887,7 +1626,7 @@ public String getAttributeTypeDisplayName() throws TskCoreException {
 	 */
 	@Deprecated
 	public String getModuleName() {
-		return sources;
+		return getSourcesCSV();
 	}
 
 }
diff --git a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties
index 5c3cab6722aa84df1482c684994d6ea1034224a2..1cfc1a6234abd0b8426689d077987689de21c094 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties
+++ b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties
@@ -314,6 +314,8 @@ IngestModuleInfo.IngestModuleType.DataSourceLevel.displayName=Data Source Level
 ReviewStatus.Approved=Approved
 ReviewStatus.Rejected=Rejected
 ReviewStatus.Undecided=Undecided
+CategoryType.ExtractedData=Extracted Data
+CategoryType.AnalysisResult=Analysis Result
 TimelineLevelOfDetail.low=Low
 TimelineLevelOfDetail.medium=Medium
 TimelineLevelOfDetail.high=High
diff --git a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties-MERGED b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties-MERGED
index 5c3cab6722aa84df1482c684994d6ea1034224a2..1cfc1a6234abd0b8426689d077987689de21c094 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties-MERGED
+++ b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties-MERGED
@@ -314,6 +314,8 @@ IngestModuleInfo.IngestModuleType.DataSourceLevel.displayName=Data Source Level
 ReviewStatus.Approved=Approved
 ReviewStatus.Rejected=Rejected
 ReviewStatus.Undecided=Undecided
+CategoryType.ExtractedData=Extracted Data
+CategoryType.AnalysisResult=Analysis Result
 TimelineLevelOfDetail.low=Low
 TimelineLevelOfDetail.medium=Medium
 TimelineLevelOfDetail.high=High
diff --git a/bindings/java/src/org/sleuthkit/datamodel/CaseDatabaseFactory.java b/bindings/java/src/org/sleuthkit/datamodel/CaseDatabaseFactory.java
index eca9630950c463208eac5a2b6e5c4367ecebbc3f..18c09c20d8db47778709aa559bc2ca08904a472e 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/CaseDatabaseFactory.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/CaseDatabaseFactory.java
@@ -147,10 +147,12 @@ private void addTables(Connection conn) throws TskCoreException {
 		try (Statement stmt = conn.createStatement()) {
 			createFileTables(stmt);
 			createArtifactTables(stmt);
+			createAnalysisResultsTables(stmt);
 			createTagTables(stmt);
 			createIngestTables(stmt);
 			createAccountTables(stmt);
 			createEventTables(stmt);
+			createAttributeTables(stmt);
 		} catch (SQLException ex) {
 			throw new TskCoreException("Error initializing tables", ex);
 		}
@@ -182,7 +184,9 @@ private void createFileTables(Statement stmt) throws SQLException {
 				+ "pool_type INTEGER NOT NULL, FOREIGN KEY(obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE);");
 
 		stmt.execute("CREATE TABLE data_source_info (obj_id " + dbQueryHelper.getBigIntType() + " PRIMARY KEY, device_id TEXT NOT NULL, "
-				+ "time_zone TEXT NOT NULL, acquisition_details TEXT, FOREIGN KEY(obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE)");
+				+ "time_zone TEXT NOT NULL, acquisition_details TEXT, added_date_time "+ dbQueryHelper.getBigIntType() + ", "
+				+ "acquisition_tool_settings TEXT, acquisition_tool_name TEXT, acquisition_tool_version TEXT, "
+				+ "FOREIGN KEY(obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE)");
 
 		stmt.execute("CREATE TABLE tsk_fs_info (obj_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, "
 				+ "data_source_obj_id " + dbQueryHelper.getBigIntType() + " NOT NULL, "
@@ -233,7 +237,8 @@ private void createFileTables(Statement stmt) throws SQLException {
 	
 	private void createArtifactTables(Statement stmt) throws SQLException {
 		stmt.execute("CREATE TABLE blackboard_artifact_types (artifact_type_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, "
-				+ "type_name TEXT NOT NULL, display_name TEXT)");
+				+ "type_name TEXT NOT NULL, display_name TEXT,"
+				+ "category_type INTEGER DEFAULT 0)");
 
 		stmt.execute("CREATE TABLE blackboard_attribute_types (attribute_type_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, "
 				+ "type_name TEXT NOT NULL, display_name TEXT, value_type INTEGER NOT NULL)");
@@ -248,6 +253,7 @@ private void createArtifactTables(Statement stmt) throws SQLException {
 				+ "data_source_obj_id " + dbQueryHelper.getBigIntType() + " NOT NULL, "
 				+ "artifact_type_id " + dbQueryHelper.getBigIntType() + " NOT NULL, "
 				+ "review_status_id INTEGER NOT NULL, "
+				+ "UNIQUE (artifact_obj_id),"
 				+ "FOREIGN KEY(obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE, "
 				+ "FOREIGN KEY(artifact_obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE, "
 				+ "FOREIGN KEY(data_source_obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE, "
@@ -268,6 +274,26 @@ private void createArtifactTables(Statement stmt) throws SQLException {
 				+ "FOREIGN KEY(attribute_type_id) REFERENCES blackboard_attribute_types(attribute_type_id))");		
 	}
 	
+	private void createAnalysisResultsTables(Statement stmt) throws SQLException  {
+		stmt.execute("CREATE TABLE tsk_analysis_results (obj_id " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "conclusion TEXT, "
+				+ "significance INTEGER NOT NULL, "
+				+ "confidence INTEGER NOT NULL, "
+				+ "configuration TEXT, justification TEXT, "
+				+ "ignore_score INTEGER DEFAULT 0, " // boolean	
+				+ "FOREIGN KEY(obj_id) REFERENCES blackboard_artifacts(artifact_obj_id) ON DELETE CASCADE"
+				+ ")");		
+		
+		stmt.execute("CREATE TABLE tsk_aggregate_score( obj_id " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "data_source_obj_id " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "significance INTEGER NOT NULL, "
+				+ "confidence INTEGER NOT NULL, "
+				+ "UNIQUE (obj_id),"
+				+ "FOREIGN KEY(obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE, "
+				+ "FOREIGN KEY(data_source_obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE "
+				+ ")");	
+		
+	}
 	private void createTagTables(Statement stmt) throws SQLException {
 		stmt.execute("CREATE TABLE tsk_tag_sets (tag_set_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, name TEXT UNIQUE)");
 		stmt.execute("CREATE TABLE tag_names (tag_name_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, display_name TEXT UNIQUE, "
@@ -332,6 +358,11 @@ private void addIndexes(Connection conn) throws TskCoreException {
 			stmt.execute("CREATE INDEX events_artifact_id ON tsk_event_descriptions(artifact_id)");
 			stmt.execute("CREATE INDEX events_sub_type_time ON tsk_events(event_type_id,  time)");
 			stmt.execute("CREATE INDEX events_time ON tsk_events(time)");
+			
+			// analysis results and scores indices
+			stmt.execute("CREATE INDEX score_significance_confidence ON tsk_aggregate_score(significance,confidence)");
+			stmt.execute("CREATE INDEX score_datasource_obj_id ON tsk_aggregate_score(data_source_obj_id)");
+			
 		} catch (SQLException ex) {
 			throw new TskCoreException("Error initializing db_info tables", ex);
 		}
@@ -430,6 +461,20 @@ private void createEventTables(Statement stmt) throws SQLException {
 			+ " time " + dbQueryHelper.getBigIntType() + " NOT NULL , "
 			+ " UNIQUE (event_type_id, event_description_id, time))");			
 	}
+
+	private void createAttributeTables(Statement stmt) throws SQLException {
+		/*
+		 * Binary representation of BYTEA is a bunch of bytes, which could
+		 * include embedded nulls so we have to pay attention to field length.
+		 * http://www.postgresql.org/docs/9.4/static/libpq-example.html
+		 */
+		stmt.execute("CREATE TABLE tsk_file_attributes (obj_id " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "attribute_type_id " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "value_type INTEGER NOT NULL, value_byte " + dbQueryHelper.getBlobType() + ", "
+				+ "value_text TEXT, value_int32 INTEGER, value_int64 " + dbQueryHelper.getBigIntType() + ", value_double NUMERIC(20, 10), "
+				+ "FOREIGN KEY(obj_id) REFERENCES tsk_files(obj_id) ON DELETE CASCADE, "
+				+ "FOREIGN KEY(attribute_type_id) REFERENCES blackboard_attribute_types(attribute_type_id))");
+	}
 	
 	/**
 	 * Helper class for holding code unique to each database type.
diff --git a/bindings/java/src/org/sleuthkit/datamodel/Content.java b/bindings/java/src/org/sleuthkit/datamodel/Content.java
index caa02b9338ce51eb361d3698d9f2cc61a372f614..e31e6ad3e026c4720b0f5e9f536d7fc35bbb907e 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/Content.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/Content.java
@@ -19,6 +19,7 @@
 package org.sleuthkit.datamodel;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 import java.util.Set;
 
@@ -174,6 +175,35 @@ public interface Content extends SleuthkitVisitableItem {
 	 */
 	public BlackboardArtifact newArtifact(BlackboardArtifact.ARTIFACT_TYPE type) throws TskCoreException;
 
+	/**
+	 * Create and add an analysis result associated with this content.
+	 *
+	 *
+	 * @param artifactType	  Type of analysis result artifact to create.
+	 * @param score          Score associated with this analysis.
+	 * @param conclusion     Conclusion from the analysis, may be empty.
+	 * @param configuration  Configuration element associated with this
+	 *                       analysis, may be empty.
+	 * @param justification	 Justification
+	 * @param attributesList Additional attributes to attach to this analysis
+	 *                       result artifact.
+	 *
+	 * @return AnalysisResultAdded The analysis return added and the
+         current aggregate score of content.
+	 *
+	 * @throws TskCoreException if critical error occurred within tsk core.
+	 */
+	public AnalysisResultAdded newAnalysisResult(BlackboardArtifact.Type artifactType, Score score, String conclusion, String configuration, String justification, Collection<BlackboardAttribute> attributesList) throws TskCoreException;
+
+	/**
+	 * Returns the final score for the content object.
+	 * 
+	 * @return Score.
+	 * 
+	 * @throws TskCoreException if critical error occurred within tsk core.
+	 */
+	public Score getAggregateScore() throws TskCoreException;
+	
 	/**
 	 * Get all artifacts associated with this content that have the given type
 	 * name
@@ -186,6 +216,17 @@ public interface Content extends SleuthkitVisitableItem {
 	 */
 	public ArrayList<BlackboardArtifact> getArtifacts(String artifactTypeName) throws TskCoreException;
 
+	/**
+	 * Get all analysis results associated with this content, that have the given type.
+	 *
+	 * @param artifactType  Type to look up.
+	 *
+	 * @return A list of analysis result artifacts matching the type.
+	 *
+	 * @throws TskCoreException If critical error occurred within tsk core.
+	 */
+	public List<AnalysisResult> getAnalysisResults(BlackboardArtifact.Type artifactType) throws TskCoreException;
+	
 	/**
 	 * Return the TSK_GEN_INFO artifact for the file so that individual
 	 * attributes can be added to it. Creates one if it does not already exist.
@@ -253,6 +294,15 @@ public interface Content extends SleuthkitVisitableItem {
 	 */
 	public ArrayList<BlackboardArtifact> getAllArtifacts() throws TskCoreException;
 
+	/**
+	 * Get all analysis results associated with this content.
+	 *
+	 * @return A list of analysis results.
+	 *
+	 * @throws TskCoreException If critical error occurred within tsk core.
+	 */
+	public List<AnalysisResult> getAllAnalysisResults() throws TskCoreException;
+	
 	/**
 	 * Get the names of all the hashsets that this content is in.
 	 *
diff --git a/bindings/java/src/org/sleuthkit/datamodel/DataSource.java b/bindings/java/src/org/sleuthkit/datamodel/DataSource.java
index f2633a1172998e3efc47c9dda9502d8aaae78d3f..3136038e1fd2dcf5db7a77300e5a7f162378190b 100755
--- a/bindings/java/src/org/sleuthkit/datamodel/DataSource.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/DataSource.java
@@ -74,6 +74,18 @@ public interface DataSource extends Content {
 	 * @throws TskCoreException Thrown if the data can not be written
 	 */
 	void setAcquisitionDetails(String details) throws TskCoreException;
+
+	/**
+	 * Sets the acquisition tool details such as its name, version number and
+	 * any settings used during the acquisition to acquire data.
+	 *
+	 * @param name     The name of the acquisition tool. May be NULL.
+	 * @param version  The acquisition tool version number. May be NULL.
+	 * @param settings The settings used by the acquisition tool. May be NULL.
+	 *
+	 * @throws TskCoreException Thrown if the data can not be written
+	 */
+	void setAcquisitionToolDetails(String name, String version, String settings) throws TskCoreException;
 	
 	/**
 	 * Gets the acquisition details field from the case database.
@@ -83,4 +95,40 @@ public interface DataSource extends Content {
 	 * @throws TskCoreException Thrown if the data can not be read
 	 */
 	String getAcquisitionDetails() throws TskCoreException;
+
+	/**
+	 * Gets the acquisition tool settings field from the case database.
+	 *
+	 * @return The acquisition tool settings. May be Null if not set.
+	 *
+	 * @throws TskCoreException Thrown if the data can not be read
+	 */
+	String getAcquisitionToolSettings() throws TskCoreException;
+
+	/**
+	 * Gets the acquisition tool name field from the case database.
+	 *
+	 * @return The acquisition tool name. May be Null if not set.
+	 *
+	 * @throws TskCoreException Thrown if the data can not be read
+	 */
+	String getAcquisitionToolName() throws TskCoreException;
+
+	/**
+	 * Gets the acquisition tool version field from the case database.
+	 *
+	 * @return The acquisition tool version. May be Null if not set. 
+	 *
+	 * @throws TskCoreException Thrown if the data can not be read
+	 */
+	String getAcquisitionToolVersion() throws TskCoreException;
+
+	/**
+	 * Gets the added date field from the case database.
+	 *
+	 * @return The date time when the image was added in epoch seconds.
+	 *
+	 * @throws TskCoreException Thrown if the data can not be read
+	 */
+	Long getDateAdded() throws TskCoreException;
 }
diff --git a/bindings/java/src/org/sleuthkit/datamodel/DerivedFile.java b/bindings/java/src/org/sleuthkit/datamodel/DerivedFile.java
index ef147fb10c980eb874594e42931342e0ba336dd2..0fa423bc2586592c43e6d7469fb560f0c2da138c 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/DerivedFile.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/DerivedFile.java
@@ -19,6 +19,7 @@
 package org.sleuthkit.datamodel;
 
 import java.text.MessageFormat;
+import java.util.Collections;
 import java.util.ResourceBundle;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -102,7 +103,7 @@ public class DerivedFile extends AbstractFile {
 		// through the class hierarchy contructors.
 		super(db, objId, dataSourceObjectId, TskData.TSK_FS_ATTR_TYPE_ENUM.TSK_FS_ATTR_TYPE_DEFAULT, 0,
 				name, TSK_DB_FILES_TYPE_ENUM.LOCAL, 0L, 0, dirType, metaType, dirFlag,
-				metaFlags, size, ctime, crtime, atime, mtime, (short) 0, 0, 0, md5Hash, sha256Hash, knownState, parentPath, mimeType, extension);
+				metaFlags, size, ctime, crtime, atime, mtime, (short) 0, 0, 0, md5Hash, sha256Hash, knownState, parentPath, mimeType, extension, Collections.emptyList());
 		setLocalFilePath(localPath);
 		setEncodingType(encodingType);
 	}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/Directory.java b/bindings/java/src/org/sleuthkit/datamodel/Directory.java
index 39b0c62188ef261354eedeb1596ba6f6f2fd0e6f..784e025aa8f619a70aae667122c27e06199a25a2 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/Directory.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/Directory.java
@@ -18,6 +18,7 @@
  */
 package org.sleuthkit.datamodel;
 
+import java.util.Collections;
 import org.sleuthkit.datamodel.TskData.FileKnown;
 import org.sleuthkit.datamodel.TskData.TSK_FS_ATTR_TYPE_ENUM;
 import org.sleuthkit.datamodel.TskData.TSK_FS_META_TYPE_ENUM;
@@ -86,7 +87,7 @@ public class Directory extends FsContent {
 			long ctime, long crtime, long atime, long mtime,
 			short modes, int uid, int gid,
 			String md5Hash, String sha256Hash, FileKnown knownState, String parentPath) {
-		super(db, objId, dataSourceObjectId, fsObjId, attrType, attrId, name, TskData.TSK_DB_FILES_TYPE_ENUM.FS, metaAddr, metaSeq, dirType, metaType, dirFlag, metaFlags, size, ctime, crtime, atime, mtime, modes, uid, gid, md5Hash, sha256Hash, knownState, parentPath, null, null);
+		super(db, objId, dataSourceObjectId, fsObjId, attrType, attrId, name, TskData.TSK_DB_FILES_TYPE_ENUM.FS, metaAddr, metaSeq, dirType, metaType, dirFlag, metaFlags, size, ctime, crtime, atime, mtime, modes, uid, gid, md5Hash, sha256Hash, knownState, parentPath, null, null, Collections.emptyList());
 	}
 
 	/**
diff --git a/bindings/java/src/org/sleuthkit/datamodel/File.java b/bindings/java/src/org/sleuthkit/datamodel/File.java
index c6758c0ff88657acb513b55810acbc445b518df3..9b87670b9c44243ebb5a1c6cdd16556abaeb3123 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/File.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/File.java
@@ -18,6 +18,8 @@
  */
 package org.sleuthkit.datamodel;
 
+import java.util.Collections;
+import java.util.List;
 import org.sleuthkit.datamodel.TskData.FileKnown;
 import org.sleuthkit.datamodel.TskData.TSK_FS_ATTR_TYPE_ENUM;
 import org.sleuthkit.datamodel.TskData.TSK_FS_META_TYPE_ENUM;
@@ -90,8 +92,9 @@ public class File extends FsContent {
 			long ctime, long crtime, long atime, long mtime,
 			short modes, int uid, int gid,
 			String md5Hash, String sha256Hash, FileKnown knownState, String parentPath, String mimeType,
-			String extension) {
-		super(db, objId, dataSourceObjectId, fsObjId, attrType, attrId, name, TskData.TSK_DB_FILES_TYPE_ENUM.FS, metaAddr, metaSeq, dirType, metaType, dirFlag, metaFlags, size, ctime, crtime, atime, mtime, modes, uid, gid, md5Hash, sha256Hash, knownState, parentPath, mimeType, extension);
+			String extension,
+			List<Attribute> fileAttributes) {
+		super(db, objId, dataSourceObjectId, fsObjId, attrType, attrId, name, TskData.TSK_DB_FILES_TYPE_ENUM.FS, metaAddr, metaSeq, dirType, metaType, dirFlag, metaFlags, size, ctime, crtime, atime, mtime, modes, uid, gid, md5Hash, sha256Hash, knownState, parentPath, mimeType, extension, fileAttributes);
 	}
 
 	/**
@@ -245,6 +248,6 @@ protected File(SleuthkitCase db,
 			String name, long metaAddr, int metaSeq, TSK_FS_NAME_TYPE_ENUM dirType, TSK_FS_META_TYPE_ENUM metaType,
 			TSK_FS_NAME_FLAG_ENUM dirFlag, short metaFlags, long size, long ctime, long crtime, long atime, long mtime,
 			short modes, int uid, int gid, String md5Hash, FileKnown knownState, String parentPath, String mimeType) {
-		this(db, objId, dataSourceObjectId, fsObjId, attrType, (int) attrId, name, metaAddr, metaSeq, dirType, metaType, dirFlag, metaFlags, size, ctime, crtime, atime, mtime, modes, uid, gid, md5Hash, null, knownState, parentPath, mimeType, null);
+		this(db, objId, dataSourceObjectId, fsObjId, attrType, (int) attrId, name, metaAddr, metaSeq, dirType, metaType, dirFlag, metaFlags, size, ctime, crtime, atime, mtime, modes, uid, gid, md5Hash, null, knownState, parentPath, mimeType, null, Collections.emptyList());
 	}
 }
diff --git a/bindings/java/src/org/sleuthkit/datamodel/FsContent.java b/bindings/java/src/org/sleuthkit/datamodel/FsContent.java
index 7e022bc81776885bf73c65fb3e38f368e44613f2..56e872ecaa9c2be066f583412e9ae2c48e99a856 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/FsContent.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/FsContent.java
@@ -19,6 +19,7 @@
 package org.sleuthkit.datamodel;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -123,8 +124,9 @@ public abstract class FsContent extends AbstractFile {
 			String md5Hash, String sha256Hash, FileKnown knownState,
 			String parentPath,
 			String mimeType,
-			String extension) {
-		super(db, objId, dataSourceObjectId, attrType, attrId, name, fileType, metaAddr, metaSeq, dirType, metaType, dirFlag, metaFlags, size, ctime, crtime, atime, mtime, modes, uid, gid, md5Hash, sha256Hash, knownState, parentPath, mimeType, extension);
+			String extension, 
+			List<Attribute> fileAttributes) {
+		super(db, objId, dataSourceObjectId, attrType, attrId, name, fileType, metaAddr, metaSeq, dirType, metaType, dirFlag, metaFlags, size, ctime, crtime, atime, mtime, modes, uid, gid, md5Hash, sha256Hash, knownState, parentPath, mimeType, extension, fileAttributes);
 		this.fsObjId = fsObjId;
 	}
 
@@ -385,7 +387,7 @@ public String toString(boolean preserveState) {
 			String name, long metaAddr, int metaSeq, TSK_FS_NAME_TYPE_ENUM dirType, TSK_FS_META_TYPE_ENUM metaType,
 			TSK_FS_NAME_FLAG_ENUM dirFlag, short metaFlags, long size, long ctime, long crtime, long atime, long mtime,
 			short modes, int uid, int gid, String md5Hash, FileKnown knownState, String parentPath) {
-		this(db, objId, db.getDataSourceObjectId(objId), fsObjId, attrType, (int) attrId, name, TSK_DB_FILES_TYPE_ENUM.FS, metaAddr, metaSeq, dirType, metaType, dirFlag, metaFlags, size, ctime, crtime, atime, mtime, modes, uid, gid, md5Hash, null, knownState, parentPath, null, null);
+		this(db, objId, db.getDataSourceObjectId(objId), fsObjId, attrType, (int) attrId, name, TSK_DB_FILES_TYPE_ENUM.FS, metaAddr, metaSeq, dirType, metaType, dirFlag, metaFlags, size, ctime, crtime, atime, mtime, modes, uid, gid, md5Hash, null, knownState, parentPath, null, null, Collections.emptyList());
 	}
 
 	/**
@@ -444,6 +446,6 @@ public String toString(boolean preserveState) {
 			String name, long metaAddr, int metaSeq, TSK_FS_NAME_TYPE_ENUM dirType, TSK_FS_META_TYPE_ENUM metaType,
 			TSK_FS_NAME_FLAG_ENUM dirFlag, short metaFlags, long size, long ctime, long crtime, long atime, long mtime,
 			short modes, int uid, int gid, String md5Hash, FileKnown knownState, String parentPath, String mimeType) {
-		this(db, objId, dataSourceObjectId, fsObjId, attrType, (int) attrId, name, TSK_DB_FILES_TYPE_ENUM.FS, metaAddr, metaSeq, dirType, metaType, dirFlag, metaFlags, size, ctime, crtime, atime, mtime, modes, uid, gid, md5Hash, null, knownState, parentPath, mimeType, null);
+		this(db, objId, dataSourceObjectId, fsObjId, attrType, (int) attrId, name, TSK_DB_FILES_TYPE_ENUM.FS, metaAddr, metaSeq, dirType, metaType, dirFlag, metaFlags, size, ctime, crtime, atime, mtime, modes, uid, gid, md5Hash, null, knownState, parentPath, mimeType, null, Collections.emptyList());
 	}
 }
diff --git a/bindings/java/src/org/sleuthkit/datamodel/Image.java b/bindings/java/src/org/sleuthkit/datamodel/Image.java
index a465af5975966e2f9cc12870732f1af7f1a3328c..0f71f7ccafe131b7af89e0362c00216bb144020f 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/Image.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/Image.java
@@ -514,10 +514,10 @@ public long getContentSize(SleuthkitCase sleuthkitCase) throws TskCoreException
 
 		return contentSize;
 	}
-	
+
 	/**
 	 * Sets the acquisition details field in the case database.
-	 * 
+	 *
 	 * @param details The acquisition details
 	 * 
 	 * @throws TskCoreException Thrown if the data can not be written
@@ -526,7 +526,66 @@ public long getContentSize(SleuthkitCase sleuthkitCase) throws TskCoreException
 	public void setAcquisitionDetails(String details) throws TskCoreException {
 		getSleuthkitCase().setAcquisitionDetails(this, details);
 	}
-	
+
+	/**
+	 * Sets the acquisition tool details such as its name, version number and
+	 * any settings used during the acquisition to acquire data.
+	 *
+	 * @param name     The name of the acquisition tool. May be NULL.
+	 * @param version  The acquisition tool version number. May be NULL.
+	 * @param settings The settings used by the acquisition tool. May be NULL.
+	 *
+	 * @throws TskCoreException Thrown if the data can not be written
+	 */
+	@Override
+	public void setAcquisitionToolDetails(String name, String version, String settings) throws TskCoreException {
+		getSleuthkitCase().setAcquisitionToolDetails(this, name, version, settings);
+	}
+
+	/**
+	 * Gets the acquisition tool settings field from the case database.
+	 *
+	 * @return The acquisition tool settings. May be Null if not set.
+	 *
+	 * @throws TskCoreException Thrown if the data can not be read
+	 */
+	public String getAcquisitionToolSettings() throws TskCoreException {
+		return getSleuthkitCase().getDataSourceInfoString(this, "acquisition_tool_settings");
+	}
+
+	/**
+	 * Gets the acquisition tool name field from the case database.
+	 *
+	 * @return The acquisition tool name. May be Null if not set.
+	 *
+	 * @throws TskCoreException Thrown if the data can not be read
+	 */
+	public String getAcquisitionToolName() throws TskCoreException{
+		return getSleuthkitCase().getDataSourceInfoString(this, "acquisition_tool_name");
+	}
+
+	/**
+	 * Gets the acquisition tool version field from the case database.
+	 *
+	 * @return The acquisition tool version. May be Null if not set.
+	 *
+	 * @throws TskCoreException Thrown if the data can not be read
+	 */
+	public String getAcquisitionToolVersion() throws TskCoreException {
+		return getSleuthkitCase().getDataSourceInfoString(this, "acquisition_tool_version");
+	}
+
+	/**
+	 * Gets the added date field from the case database.
+	 *
+	 * @return The date time when the image was added in epoch seconds.
+	 *
+	 * @throws TskCoreException Thrown if the data can not be read
+	 */
+	public Long getDateAdded() throws TskCoreException {
+		return getSleuthkitCase().getDataSourceInfoLong(this, "added_date_time");
+	}
+
 	/**
 	 * Gets the acquisition details field from the case database.
 	 * 
@@ -539,6 +598,23 @@ public String getAcquisitionDetails() throws TskCoreException {
 		return getSleuthkitCase().getAcquisitionDetails(this);
 	}	
 
+	/**
+	 * Updates the image's total size and sector size.This function may be used
+	 * to update the sizes after the image was created.
+	 *
+	 * Can only update the sizes if they were not set before. Will throw
+	 * TskCoreException if the values in the db are not 0 prior to this call.
+	 *
+	 * @param totalSize  The total size
+	 * @param sectorSize The sector size
+	 *
+	 * @throws TskCoreException If there is an error updating the case database.
+	 *
+	 */
+	public void setSizes(long totalSize, long sectorSize) throws TskCoreException {
+		getSleuthkitCase().setImageSizes(this, totalSize, sectorSize);
+	}
+
 	/**
 	 * Close a ResultSet.
 	 *
diff --git a/bindings/java/src/org/sleuthkit/datamodel/LayoutFile.java b/bindings/java/src/org/sleuthkit/datamodel/LayoutFile.java
index ab98c64e75a8ffc50219df350fbe2c63b2a539f6..d07412837d65d530e55a6b0e46018fd7eabd22a3 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/LayoutFile.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/LayoutFile.java
@@ -18,6 +18,7 @@
  */
 package org.sleuthkit.datamodel;
 
+import java.util.Collections;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import org.sleuthkit.datamodel.TskData.FileKnown;
@@ -93,7 +94,7 @@ public class LayoutFile extends AbstractFile {
 			long ctime, long crtime, long atime, long mtime,
 			String md5Hash, String sha256Hash, FileKnown knownState,
 			String parentPath, String mimeType) {
-		super(db, objId, dataSourceObjectId, TSK_FS_ATTR_TYPE_ENUM.TSK_FS_ATTR_TYPE_DEFAULT, 0, name, fileType, 0L, 0, dirType, metaType, dirFlag, metaFlags, size, ctime, crtime, atime, mtime, (short) 0, 0, 0, md5Hash, sha256Hash, knownState, parentPath, mimeType, SleuthkitCase.extractExtension(name));
+		super(db, objId, dataSourceObjectId, TSK_FS_ATTR_TYPE_ENUM.TSK_FS_ATTR_TYPE_DEFAULT, 0, name, fileType, 0L, 0, dirType, metaType, dirFlag, metaFlags, size, ctime, crtime, atime, mtime, (short) 0, 0, 0, md5Hash, sha256Hash, knownState, parentPath, mimeType, SleuthkitCase.extractExtension(name), Collections.emptyList());
 	}
 
 	/**
diff --git a/bindings/java/src/org/sleuthkit/datamodel/LocalFile.java b/bindings/java/src/org/sleuthkit/datamodel/LocalFile.java
index 99abf74e70cd7f77cbaa062e0fa1316072064611..152c7c3831545d5a3725dbbae6694958724229f1 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/LocalFile.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/LocalFile.java
@@ -91,7 +91,7 @@ public class LocalFile extends AbstractFile {
 			String extension) {
 		super(db, objId, dataSourceObjectId, TSK_FS_ATTR_TYPE_ENUM.TSK_FS_ATTR_TYPE_DEFAULT, 0,
 				name, fileType, 0L, 0, dirType, metaType, dirFlag,
-				metaFlags, size, ctime, crtime, atime, mtime, (short) 0, 0, 0, md5Hash, sha256Hash, knownState, parentPath, mimeType, extension);
+				metaFlags, size, ctime, crtime, atime, mtime, (short) 0, 0, 0, md5Hash, sha256Hash, knownState, parentPath, mimeType, extension, Collections.emptyList());
 		// TODO (AUT-1904): The parent id should be passed to AbstractContent 
 		// through the class hierarchy contructors, using 
 		// AbstractContent.UNKNOWN_ID as needed.
diff --git a/bindings/java/src/org/sleuthkit/datamodel/LocalFilesDataSource.java b/bindings/java/src/org/sleuthkit/datamodel/LocalFilesDataSource.java
index 0572291890859ea7ecaa7a61236cde43b1c102e5..e54430c08492b114bc711350b2627a0777b870ca 100755
--- a/bindings/java/src/org/sleuthkit/datamodel/LocalFilesDataSource.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/LocalFilesDataSource.java
@@ -180,19 +180,34 @@ static long getContentSize(SleuthkitCase sleuthkitCase, long dataSourceObjId) th
 
 		return contentSize;
 	}
-	
+
 	/**
 	 * Sets the acquisition details field in the case database.
-	 * 
+	 *
 	 * @param details The acquisition details
-	 * 
+	 *
 	 * @throws TskCoreException Thrown if the data can not be written
 	 */
 	@Override
 	public void setAcquisitionDetails(String details) throws TskCoreException {
 		getSleuthkitCase().setAcquisitionDetails(this, details);
 	}
-	
+
+	/**
+	 * Sets the acquisition tool details such as its name, version number and
+	 * any settings used during the acquisition to acquire data.
+	 *
+	 * @param name     The name of the acquisition tool. May be NULL.
+	 * @param version  The acquisition tool version number. May be NULL.
+	 * @param settings The settings used by the acquisition tool. May be NULL.
+	 *
+	 * @throws TskCoreException Thrown if the data can not be written
+	 */
+	@Override
+	public void setAcquisitionToolDetails(String name, String version, String settings) throws TskCoreException {
+		getSleuthkitCase().setAcquisitionToolDetails(this, name, version, settings);
+	}
+  
 	/**
 	 * Gets the acquisition details field from the case database.
 	 * 
@@ -205,6 +220,52 @@ public String getAcquisitionDetails() throws TskCoreException {
 		return getSleuthkitCase().getAcquisitionDetails(this);
 	}
 
+
+	/**
+	 * Gets the acquisition tool settings field from the case database.
+	 *
+	 * @return The acquisition tool settings. May be Null if not set.
+	 *
+	 * @throws TskCoreException Thrown if the data can not be read
+	 */
+	@Override
+	public String getAcquisitionToolSettings() throws TskCoreException {
+		return getSleuthkitCase().getDataSourceInfoString(this, "acquisition_tool_settings");
+	}
+
+	/**
+	 * Gets the acquisition tool name field from the case database.
+	 *
+	 * @return The acquisition tool name. May be Null if not set.
+	 *
+	 * @throws TskCoreException Thrown if the data can not be read
+	 */
+	public String getAcquisitionToolName() throws TskCoreException {
+		return getSleuthkitCase().getDataSourceInfoString(this, "acquisition_tool_name");
+	}
+
+	/**
+	 * Gets the acquisition tool version field from the case database.
+	 *
+	 * @return The acquisition tool version. May be Null if not set.
+	 *
+	 * @throws TskCoreException Thrown if the data can not be read
+	 */
+	public String getAcquisitionToolVersion() throws TskCoreException{
+		return getSleuthkitCase().getDataSourceInfoString(this, "acquisition_tool_version");
+	}
+
+	/**
+	 * Gets the added date field from the case database.
+	 *
+	 * @return The date time when the image was added in epoch seconds.
+	 *
+	 * @throws TskCoreException Thrown if the data can not be read
+	 */
+	public Long getDateAdded() throws TskCoreException {
+		return getSleuthkitCase().getDataSourceInfoLong(this, "added_date_time");
+	}
+
 	/**
 	 * Close a ResultSet.
 	 *
diff --git a/bindings/java/src/org/sleuthkit/datamodel/Report.java b/bindings/java/src/org/sleuthkit/datamodel/Report.java
index bf70b73924b2149343cf355517fda8ec49e34f9d..1db35e0562335c2692fd016ea96a4c7037a5a0bc 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/Report.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/Report.java
@@ -26,11 +26,14 @@
 import java.nio.file.Paths;
 import static java.nio.file.StandardOpenOption.READ;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 import java.util.logging.Level;
 import java.util.logging.Logger;
+import org.sleuthkit.datamodel.Blackboard.BlackboardException;
+import org.sleuthkit.datamodel.SleuthkitCase.CaseDbTransaction;
 
 /**
  * This is a class that models reports.
@@ -236,6 +239,22 @@ public BlackboardArtifact newArtifact(int artifactTypeID) throws TskCoreExceptio
 		return db.newBlackboardArtifact(artifactTypeID, objectId);
 	}
 
+
+	
+	@Override
+	public AnalysisResultAdded newAnalysisResult(BlackboardArtifact.Type artifactType, Score score, String conclusion, String configuration, String justification, Collection<BlackboardAttribute> attributesList) throws TskCoreException {
+		CaseDbTransaction trans = db.beginTransaction();
+		try {
+			AnalysisResultAdded resultAdded = db.getBlackboard().newAnalysisResult(artifactType, objectId, this.getDataSource().getId(), score, conclusion, configuration, justification, attributesList, trans);
+
+			trans.commit();
+			return resultAdded;
+		} catch (BlackboardException ex) {
+			trans.rollback();
+			throw new TskCoreException("Error adding analysis result.", ex);
+		}
+	}
+	
 	@Override
 	public BlackboardArtifact newArtifact(BlackboardArtifact.ARTIFACT_TYPE type) throws TskCoreException {
 		return newArtifact(type.getTypeID());
@@ -282,6 +301,21 @@ public ArrayList<BlackboardArtifact> getAllArtifacts() throws TskCoreException {
 		return db.getMatchingArtifacts("WHERE obj_id = " + objectId); //NON-NLS
 	}
 
+	@Override
+	public List<AnalysisResult> getAllAnalysisResults() throws TskCoreException {
+		return db.getBlackboard().getAnalysisResults(objectId);
+	}
+	
+	@Override
+	public List<AnalysisResult> getAnalysisResults(BlackboardArtifact.Type artifactType) throws TskCoreException {
+		return db.getBlackboard().getAnalysisResults(objectId,  artifactType.getTypeID());
+	}
+	
+	@Override
+	public Score getAggregateScore() throws TskCoreException {
+		return db.getScoringManager().getAggregateScore(objectId);
+	}
+	
 	@Override
 	public Set<String> getHashSetNames() throws TskCoreException {
 		return Collections.<String>emptySet();
diff --git a/bindings/java/src/org/sleuthkit/datamodel/Score.java b/bindings/java/src/org/sleuthkit/datamodel/Score.java
new file mode 100644
index 0000000000000000000000000000000000000000..4b2414dd15f5db698626a459b98ab42612a92040
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/Score.java
@@ -0,0 +1,161 @@
+/*
+ * 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 java.util.Arrays;
+import java.util.Comparator;
+
+/**
+ *
+ * Encapsulates a final score of an artifact. Computed by taking into account
+ * all of the analysis results.
+ */
+public class Score implements Comparable<Score> {
+
+	/**
+	 * Enum to encapsulate significance of a analysis result.
+	 *
+	 */
+	public enum Significance {
+
+		NONE(0, "None"),		// has no (bad) significance, i.e. it's Good
+		UNKNOWN(10, "Unknown"), // no analysis has been performed to ascertain significance.
+		LOW(20, "Low"),
+		MEDIUM(30, "Medium"), // Suspicious
+		HIGH(40, "High");	// Bad & Notable
+
+		private final int id;
+		private final String name;
+
+		private Significance(int id, String name) {
+			this.id = id;
+			this.name = name;
+		}
+
+		public static Significance fromString(String name) {
+			return Arrays.stream(values())
+					.filter(val -> val.getName().equals(name))
+					.findFirst().orElse(NONE);
+		}
+
+		static public Significance fromID(int id) {
+			return Arrays.stream(values())
+					.filter(val -> val.getId() == id)
+					.findFirst().orElse(NONE);
+		}
+
+		public int getId() {
+			return id;
+		}
+
+		public String getName() {
+			return name;
+		}
+
+		@Override
+		public String toString() {
+			return name;
+		}
+	}
+
+	/**
+	 * Encapsulates confidence in a analysis result.
+	 *
+	 * Higher confidence implies fewer false positives.
+	 */
+	public enum Confidence {
+
+		NONE(0, "None"),
+		LOWEST(10, "Lowest"),
+		LOW(20, "Low"),
+		MEDIUM(30, "Medium"),
+		HIGH(40, "High"),
+		HIGHEST(50, "Highest");
+
+		private final int id;
+		private final String name;
+
+		private Confidence(int id, String name) {
+			this.id = id;
+			this.name = name;
+		}
+
+		public static Confidence fromString(String name) {
+			return Arrays.stream(values())
+					.filter(val -> val.getName().equals(name))
+					.findFirst().orElse(NONE);
+		}
+
+		static public Confidence fromID(int id) {
+			return Arrays.stream(values())
+					.filter(val -> val.getId() == id)
+					.findFirst().orElse(NONE);
+		}
+
+		public int getId() {
+			return id;
+		}
+
+		public String getName() {
+			return name;
+		}
+
+		@Override
+		public String toString() {
+			return name;
+		}
+	}
+
+	public static final Score SCORE_UNKNOWN = new Score(Significance.UNKNOWN, Confidence.NONE);
+	
+	// Score is a combination of significance and confidence.
+	private final Significance significance;
+	private final Confidence confidence;
+
+	public Score(Significance significance, Confidence confidence) {
+		this.significance = significance;
+		this.confidence = confidence;
+	}
+
+	public Significance getSignificance() {
+		return significance;
+	}
+
+	public Confidence getConfidence() {
+		return confidence;
+	}
+
+	@Override
+	public int compareTo(Score other) {
+		// A score is a combination of significance & confidence
+		// Higher confidence wins.  
+		// If two results have same confidence, then the higher significance wins
+		if (this.getConfidence() != other.getConfidence()) {
+			return this.getConfidence().ordinal() - other.getConfidence().ordinal();
+		} else {
+			return this.getSignificance().ordinal() - other.getSignificance().ordinal();
+		}
+	}
+	
+	 public static final Comparator<Score> getScoreComparator() {
+        return (Score score1, Score score2) -> {
+			return score1.compareTo(score2);
+        };
+    }
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/ScoreChange.java b/bindings/java/src/org/sleuthkit/datamodel/ScoreChange.java
new file mode 100644
index 0000000000000000000000000000000000000000..d30becf5ce25c65a2ef281e3352cac46e369d7b6
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/ScoreChange.java
@@ -0,0 +1,53 @@
+/*
+ * 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;
+
+/**
+ * This class encapsulates a score change.
+ */
+final public class ScoreChange {
+
+	private final long objId;
+	private final long dataSourceObjectId;
+	private final Score oldScore;
+	private final Score newScore;
+
+	ScoreChange(long objId, long dataSourceObjectId, Score oldScore, Score newScore) {
+		this.objId = objId;
+		this.dataSourceObjectId = dataSourceObjectId;
+		this.oldScore = oldScore;
+		this.newScore = newScore;
+	}
+
+	public long getDataSourceObjectId() {
+		return dataSourceObjectId;
+	}
+
+	public long getObjId() {
+		return objId;
+	}
+
+	public Score getOldScore() {
+		return oldScore;
+	}
+
+	public Score getNewScore() {
+		return newScore;
+	}
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/ScoringManager.java b/bindings/java/src/org/sleuthkit/datamodel/ScoringManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..217722839d4e8877c6a6ff7f126940313cf6fca8
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/ScoringManager.java
@@ -0,0 +1,294 @@
+/*
+ * 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 java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Logger;
+import org.sleuthkit.datamodel.Score.Confidence;
+import org.sleuthkit.datamodel.Score.Significance;
+import org.sleuthkit.datamodel.SleuthkitCase.CaseDbConnection;
+import org.sleuthkit.datamodel.SleuthkitCase.CaseDbTransaction;
+
+/**
+ * The scoring manager is responsible for updating and querying the score of
+ * objects.
+ *
+ */
+public class ScoringManager {
+
+	private static final Logger LOGGER = Logger.getLogger(ScoringManager.class.getName());
+
+	private final SleuthkitCase db;
+
+	/**
+	 * Construct a ScoringManager for the given SleuthkitCase.
+	 *
+	 * @param skCase The SleuthkitCase
+	 *
+	 */
+	ScoringManager(SleuthkitCase skCase) {
+		this.db = skCase;
+	}
+
+	/**
+	 * Get the aggregate score for the given object.
+	 *
+	 * @param objId Object id.
+	 *
+	 * @return Score, if it is found, unknown otherwise.
+	 *
+	 * @throws TskCoreException
+	 */
+	public Score getAggregateScore(long objId) throws TskCoreException {
+		try (CaseDbConnection connection = db.getConnection()) {
+			return getAggregateScore(objId, connection);
+		}
+	}
+
+	/**
+	 * Get the aggregate score for the given object. Uses the connection from the
+	 * given transaction.
+	 *
+	 * @param objId      Object id.
+	 * @param transaction Transaction that provides the connection to use.
+	 *
+	 * @return Score, if it is found, unknown otherwise.
+	 *
+	 * @throws TskCoreException
+	 */
+	private Score getAggregateScore(long objId, CaseDbTransaction transaction) throws TskCoreException {
+		CaseDbConnection connection = transaction.getConnection();
+		return getAggregateScore(objId, connection);
+	}
+
+	/**
+	 * Get the aggregate score for the given object.
+	 *
+	 * @param objId Object id.
+	 * @param connection Connection to use for the query.
+	 *
+	 * @return Score, if it is found, Score(UNKNOWN,NONE) otherwise.
+	 *
+	 * @throws TskCoreException
+	 */
+	private Score getAggregateScore(long objId, CaseDbConnection connection) throws TskCoreException {
+		String queryString = "SELECT significance, confidence FROM tsk_aggregate_score WHERE obj_id = " + objId;
+
+		try {
+			db.acquireSingleUserCaseReadLock();
+
+			try (Statement s = connection.createStatement(); ResultSet rs = connection.executeQuery(s, queryString)) {
+				if (rs.next()) {
+					return new Score(Significance.fromID(rs.getInt("significance")), Confidence.fromID(rs.getInt("confidence")));
+				} else {
+					return new Score(Significance.UNKNOWN, Confidence.NONE);
+				}
+			} catch (SQLException ex) {
+				throw new TskCoreException("SQLException thrown while running query: " + queryString, ex);
+			}
+		} finally {
+			db.releaseSingleUserCaseReadLock();
+		}
+	}
+
+	/**
+	 * Inserts or updates the score for the given object.
+	 *
+	 * @param objId Object id of the object.
+	 * @param dataSourceObjectId Data source object id.
+	 * @param score  Score to be inserted/updated.
+	 * @param transaction Transaction to use for the update.
+	 *
+	 * @throws TskCoreException
+	 */
+	private void setAggregateScore(long objId, long dataSourceObjectId, Score score, CaseDbTransaction transaction) throws TskCoreException {
+		CaseDbConnection connection = transaction.getConnection();
+		setAggregateScore(objId, dataSourceObjectId, score, connection);
+	}
+
+	/**
+	 * Inserts or updates the score for the given object.
+	 *
+	 * @param objId Object id of the object.
+	 * @param dataSourceObjectId Data source object id.
+	 * @param score  Score to be inserted/updated.
+	 * @param connection Connection to use for the update.
+	 *
+	 * @throws TskCoreException
+	 */
+	private void setAggregateScore(long objId, long dataSourceObjectId, Score score, CaseDbConnection connection) throws TskCoreException {
+
+		String query = String.format("INSERT INTO tsk_aggregate_score (obj_id, data_source_obj_id, significance , confidence) VALUES (%d, %d, %d, %d)"
+				+ " ON CONFLICT (obj_id) DO UPDATE SET significance = %d, confidence = %d",
+				objId, dataSourceObjectId, score.getSignificance().getId(), score.getConfidence().getId(), score.getSignificance().getId(), score.getConfidence().getId() );
+
+		try {
+			db.acquireSingleUserCaseWriteLock();
+
+			try (Statement updateStatement = connection.createStatement()) {
+				updateStatement.executeUpdate(query);
+			} catch (SQLException ex) {
+				throw new TskCoreException("Error updating  aggregate score, query: " + query, ex);//NON-NLS
+			}
+
+		} finally {
+			db.releaseSingleUserCaseWriteLock();
+		}
+
+	}
+
+
+
+	/**
+	 * Updates the score for the specified object, if the given analysis result
+	 * score is higher than the score the object already has.
+	 *
+	 * @param objId      Object id.
+	 * @param dataSourceObjectId Object id of the data source.
+	 * @param resultScore Score for a newly added analysis result.
+	 * @param transaction Transaction to use for the update.
+	 *
+	 * @return Aggregate score for the object.
+	 *
+	 * @throws TskCoreException
+	 */
+	Score updateAggregateScore(long objId, long dataSourceObjectId, Score resultScore, CaseDbTransaction transaction) throws TskCoreException {
+
+		// Get the current score 
+		Score currentScore = ScoringManager.this.getAggregateScore(objId, transaction);
+
+		// If current score is Unknown And newscore is not Unknown - allow None (good) to be recorded
+		// or if the new score is higher than the current score
+		if  ( (currentScore.compareTo(Score.SCORE_UNKNOWN) == 0 && resultScore.compareTo(Score.SCORE_UNKNOWN) != 0)
+			  || (Score.getScoreComparator().compare(resultScore, currentScore) > 0)) {
+			ScoringManager.this.setAggregateScore(objId, dataSourceObjectId, resultScore, transaction);
+			
+			// register score change in the transaction.
+			transaction.registerScoreChange(new ScoreChange(objId, dataSourceObjectId, currentScore, resultScore));
+			return resultScore;
+		} else {
+			// return the current score
+			return currentScore;
+		}
+	}
+
+	/**
+	 * Get the count of contents within the specified data source
+	 * with the specified aggregate score.
+	 *
+	 * @param dataSourceObjectId Data source object id.
+	 * @param aggregateScore Score to look for.
+	 *
+	 * @return Number of contents with given score.
+	 * @throws TskCoreException if there is an error getting the count. 
+	 */
+	public long getContentCount(long dataSourceObjectId, Score aggregateScore) throws TskCoreException {
+		try (CaseDbConnection connection = db.getConnection()) {
+			return getContentCount(dataSourceObjectId, aggregateScore, connection);
+		} 
+	}
+
+
+	/**
+	 * Get the count of contents with the specified score. Uses the specified
+	 * transaction to obtain the database connection.
+	 *
+	 * @param dataSourceObjectId Data source object id.
+	 * @param aggregateScore       Score to look for.
+	 * @param transaction Transaction from which to get the connection.
+	 *
+	 * @return Number of contents with given score.
+	 *
+	 * @throws TskCoreException if there is an error getting the count. 
+	 */
+	private long getContentCount(long dataSourceObjectId, Score aggregateScore, CaseDbConnection connection) throws TskCoreException {
+		String queryString = "SELECT COUNT(obj_id) AS count FROM tsk_aggregate_score"
+				+ " WHERE data_source_obj_id = " + dataSourceObjectId 
+				+ " AND significance = " + aggregateScore.getSignificance().getId()
+				+ " AND confidence = " + aggregateScore.getConfidence().getId();
+
+		db.acquireSingleUserCaseReadLock();
+		try (Statement statement = connection.createStatement();
+				ResultSet resultSet = connection.executeQuery(statement, queryString);) {
+
+			long count = 0;
+			if (resultSet.next()) {
+				count = resultSet.getLong("count");
+			}
+			return count;
+		} catch (SQLException ex) {
+			throw new TskCoreException("Error getting count of items with score = " + aggregateScore.toString(), ex);
+		} finally {
+			db.releaseSingleUserCaseReadLock();
+		}
+	}
+	
+	/**
+	 * Get the contents with the specified score.
+	 * 
+	 * @param dataSourceObjectId Data source object id.
+	 * @param aggregateScore Score to look for.
+	 *
+	 * @return Collection of contents with given score.
+	 */
+	public List<Content> getContent(long dataSourceObjectId, Score aggregateScore) throws TskCoreException {
+		try (CaseDbConnection connection = db.getConnection()) {
+			return getContent(dataSourceObjectId, aggregateScore, connection);
+		} 
+	}
+
+	/**
+	 * Gets the contents with the specified score. Uses the specified transaction
+	 * to obtain the database connection.
+	 *
+	 * @param dataSourceObjectId Data source object id.
+	 * @param aggregateScore       Score to look for.
+	 * @param connection Connection to use for the query.
+	 *
+	 * @return List of contents with given score.
+	 *
+	 * @throws TskCoreException
+	 */
+	private List<Content> getContent(long dataSourceObjectId, Score aggregateScore, CaseDbConnection connection) throws TskCoreException {
+		String queryString = "SELECT obj_id FROM tsk_aggregate_score"
+				+ " WHERE data_source_obj_id = " + dataSourceObjectId 
+				+ " AND significance = " + aggregateScore.getSignificance().getId()
+				+ " AND confidence = " + aggregateScore.getConfidence().getId();
+
+		db.acquireSingleUserCaseReadLock();
+		try (Statement statement = connection.createStatement();
+				ResultSet resultSet = connection.executeQuery(statement, queryString);) {
+
+			List<Content> items = new ArrayList<>();
+			while (resultSet.next()) {
+				long objId = resultSet.getLong("obj_id");
+				items.add(db.getContentById(objId));
+			}
+			return items;
+		} catch (SQLException ex) {
+			throw new TskCoreException("Error getting list of items with score = " + aggregateScore.toString(), ex);
+		} finally {
+			db.releaseSingleUserCaseReadLock();
+		}
+	}
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/SlackFile.java b/bindings/java/src/org/sleuthkit/datamodel/SlackFile.java
index df038e6021013a8f0a5b706e27721a721b2856d5..161c21d65b8c2a42f4d1642324d50d10a95f6267 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/SlackFile.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/SlackFile.java
@@ -18,6 +18,7 @@
  */
 package org.sleuthkit.datamodel;
 
+import java.util.Collections;
 import org.sleuthkit.datamodel.TskData.FileKnown;
 import org.sleuthkit.datamodel.TskData.TSK_FS_ATTR_TYPE_ENUM;
 import org.sleuthkit.datamodel.TskData.TSK_FS_META_TYPE_ENUM;
@@ -91,7 +92,7 @@ public class SlackFile extends FsContent {
 			short modes, int uid, int gid,
 			String md5Hash, String sha256Hash, FileKnown knownState, String parentPath, String mimeType,
 			String extension) {
-		super(db, objId, dataSourceObjectId, fsObjId, attrType, attrId, name, TskData.TSK_DB_FILES_TYPE_ENUM.SLACK, metaAddr, metaSeq, dirType, metaType, dirFlag, metaFlags, size, ctime, crtime, atime, mtime, modes, uid, gid, md5Hash, sha256Hash, knownState, parentPath, mimeType, extension);
+		super(db, objId, dataSourceObjectId, fsObjId, attrType, attrId, name, TskData.TSK_DB_FILES_TYPE_ENUM.SLACK, metaAddr, metaSeq, dirType, metaType, dirFlag, metaFlags, size, ctime, crtime, atime, mtime, modes, uid, gid, md5Hash, sha256Hash, knownState, parentPath, mimeType, extension, Collections.emptyList());
 	}
 
 	/**
diff --git a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
index 40436741704157b731d3ee429878a5cb251f6c23..0ed5a405299f69681ecd1920a0dd0b27f4fd2c06 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
@@ -65,6 +65,7 @@
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 import java.util.logging.Level;
 import java.util.logging.Logger;
+import java.util.stream.Collectors;
 import org.postgresql.util.PSQLState;
 import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE;
 import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE;
@@ -98,7 +99,7 @@ public class SleuthkitCase {
 	 * tsk/auto/tsk_db.h.
 	 */
 	static final CaseDbSchemaVersionNumber CURRENT_DB_SCHEMA_VERSION
-			= new CaseDbSchemaVersionNumber(8, 6);
+			= new CaseDbSchemaVersionNumber(8, 7);
 
 	private static final long BASE_ARTIFACT_ID = Long.MIN_VALUE; // Artifact ids will start at the lowest negative value
 	private static final Logger logger = Logger.getLogger(SleuthkitCase.class.getName());
@@ -210,6 +211,7 @@ public class SleuthkitCase {
 	private Blackboard blackboard;
 	private CaseDbAccessManager dbAccessManager;
 	private TaggingManager taggingMgr;
+	private ScoringManager scoringManager;
 
 	private final Map<String, Set<Long>> deviceIdToDatasourceObjIdMap = new HashMap<>();
 
@@ -388,6 +390,7 @@ private void init() throws Exception {
 		timelineMgr = new TimelineManager(this);
 		dbAccessManager = new CaseDbAccessManager(this);
 		taggingMgr = new TaggingManager(this);
+		scoringManager = new ScoringManager(this);
 	}
 
 	/**
@@ -500,6 +503,17 @@ public synchronized TaggingManager getTaggingManager() {
 		return taggingMgr;
 	}
 
+	/**
+	 * Gets the scoring manager for this case.
+	 *
+	 * @return The per case ScoringManager object.
+	 *
+	 * @throws org.sleuthkit.datamodel.TskCoreException
+	 */
+	public ScoringManager getScoringManager() throws TskCoreException {
+		return scoringManager;
+	}
+	
 	/**
 	 * Make sure the predefined artifact types are in the artifact types table.
 	 *
@@ -515,7 +529,7 @@ private void initBlackboardArtifactTypes() throws SQLException, TskCoreException
 			statement = connection.createStatement();
 			for (ARTIFACT_TYPE type : ARTIFACT_TYPE.values()) {
 				try {
-					statement.execute("INSERT INTO blackboard_artifact_types (artifact_type_id, type_name, display_name) VALUES (" + type.getTypeID() + " , '" + type.getLabel() + "', '" + type.getDisplayName() + "')"); //NON-NLS
+					statement.execute("INSERT INTO blackboard_artifact_types (artifact_type_id, type_name, display_name, category_type) VALUES (" + type.getTypeID() + " , '" + type.getLabel() + "', '" + type.getDisplayName() + "' , " + type.getCategory().getID() + ")"); //NON-NLS
 				} catch (SQLException ex) {
 					resultSet = connection.executeQuery(statement, "SELECT COUNT(*) AS count FROM blackboard_artifact_types WHERE artifact_type_id = '" + type.getTypeID() + "'"); //NON-NLS
 					resultSet.next();
@@ -915,6 +929,7 @@ private void updateDatabaseSchema(String dbPath) throws Exception {
 				dbSchemaVersion = updateFromSchema8dot3toSchema8dot4(dbSchemaVersion, connection);
 				dbSchemaVersion = updateFromSchema8dot4toSchema8dot5(dbSchemaVersion, connection);
 				dbSchemaVersion = updateFromSchema8dot5toSchema8dot6(dbSchemaVersion, connection);
+				dbSchemaVersion = updateFromSchema8dot6toSchema8dot7(dbSchemaVersion, connection);
 				statement = connection.createStatement();
 				connection.executeUpdate(statement, "UPDATE tsk_db_info SET schema_ver = " + dbSchemaVersion.getMajor() + ", schema_minor_ver = " + dbSchemaVersion.getMinor()); //NON-NLS
 				connection.executeUpdate(statement, "UPDATE tsk_db_info_extended SET value = " + dbSchemaVersion.getMajor() + " WHERE name = '" + SCHEMA_MAJOR_VERSION_KEY + "'"); //NON-NLS
@@ -2214,6 +2229,35 @@ private CaseDbSchemaVersionNumber updateFromSchema8dot5toSchema8dot6(CaseDbSchem
 		}
 	}	
 
+	private CaseDbSchemaVersionNumber updateFromSchema8dot6toSchema8dot7(CaseDbSchemaVersionNumber schemaVersion, CaseDbConnection connection) throws SQLException, TskCoreException {
+		if (schemaVersion.getMajor() != 8) {
+			return schemaVersion;
+		}
+
+		if (schemaVersion.getMinor() != 6) {
+			return schemaVersion;
+		}
+
+		Statement statement = connection.createStatement();
+		acquireSingleUserCaseWriteLock();
+		try {
+			String dateDataType = "BIGINT";
+			if (this.dbType.equals(DbType.SQLITE)) {
+				dateDataType = "INTEGER";
+			}
+			statement.execute("ALTER TABLE data_source_info ADD COLUMN added_date_time "+ dateDataType);
+			statement.execute("ALTER TABLE data_source_info ADD COLUMN acquisition_tool_settings TEXT");
+			statement.execute("ALTER TABLE data_source_info ADD COLUMN acquisition_tool_name TEXT");
+			statement.execute("ALTER TABLE data_source_info ADD COLUMN acquisition_tool_version TEXT");
+
+			return new CaseDbSchemaVersionNumber(8, 7);
+
+		} finally {
+			closeStatement(statement);
+			releaseSingleUserCaseWriteLock();
+		}
+	}
+
 	/**
 	 * Inserts a row for the given account type in account_types table, if one
 	 * doesn't exist.
@@ -3159,6 +3203,7 @@ public List<BlackboardArtifact> getBlackboardArtifacts(BlackboardAttribute.ATTRI
 		}
 	}
 
+	
 	/**
 	 * Get all blackboard artifacts that have an attribute of the given type and
 	 * String value. Does not included rejected artifacts.
@@ -3936,7 +3981,7 @@ public void addBlackboardAttributes(Collection<BlackboardAttribute> attributes,
 		}
 	}
 
-	private void addBlackBoardAttribute(BlackboardAttribute attr, int artifactTypeId, CaseDbConnection connection) throws SQLException, TskCoreException {
+	void addBlackBoardAttribute(BlackboardAttribute attr, int artifactTypeId, CaseDbConnection connection) throws SQLException, TskCoreException {
 		PreparedStatement statement;
 		switch (attr.getAttributeType().getValueType()) {
 			case STRING:
@@ -3981,6 +4026,49 @@ private void addBlackBoardAttribute(BlackboardAttribute attr, int artifactTypeId
 		statement.setLong(6, attr.getAttributeType().getValueType().getType());
 		connection.executeUpdate(statement);
 	}
+	
+	void addFileAttribute(Attribute attr, CaseDbConnection connection) throws SQLException, TskCoreException {
+		PreparedStatement statement;
+		statement = connection.getPreparedStatement(PREPARED_STATEMENT.INSERT_FILE_ATTRIBUTE);
+		statement.clearParameters();
+
+		statement.setLong(1, attr.getAttributeOwnerId());
+		statement.setInt(2, attr.getAttributeType().getTypeID());
+		statement.setLong(3, attr.getAttributeType().getValueType().getType());
+
+		if (attr.getAttributeType().getValueType() == TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.BYTE) {
+			statement.setBytes(4, attr.getValueBytes());
+		} else {
+			statement.setBytes(4, null);
+		}
+
+		if (attr.getAttributeType().getValueType() == TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING
+				|| attr.getAttributeType().getValueType() == TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON) {
+			statement.setString(5, attr.getValueString());
+		} else {
+			statement.setString(5, null);
+		}
+		if (attr.getAttributeType().getValueType() == TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.INTEGER) {
+			statement.setInt(6, attr.getValueInt());
+		} else {
+			statement.setNull(6, java.sql.Types.INTEGER);
+		}
+ 
+		if (attr.getAttributeType().getValueType() == TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DATETIME
+				|| attr.getAttributeType().getValueType() == TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.LONG) {
+			statement.setLong(7, attr.getValueLong());
+		} else {
+			statement.setNull(7, java.sql.Types.BIGINT);
+		}
+		
+		if (attr.getAttributeType().getValueType() == TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DOUBLE) {
+			statement.setDouble(8, attr.getValueDouble());
+		} else {
+			statement.setNull(8, java.sql.Types.DOUBLE);
+		}
+ 
+		connection.executeUpdate(statement);
+	}
 
 	/**
 	 * Adds a source name to the source column of one or more rows in the
@@ -4255,11 +4343,11 @@ public BlackboardArtifact.Type getArtifactType(String artTypeName) throws TskCor
 		ResultSet rs = null;
 		try {
 			s = connection.createStatement();
-			rs = connection.executeQuery(s, "SELECT artifact_type_id, type_name, display_name FROM blackboard_artifact_types WHERE type_name = '" + artTypeName + "'"); //NON-NLS
+			rs = connection.executeQuery(s, "SELECT artifact_type_id, type_name, display_name, category_type FROM blackboard_artifact_types WHERE type_name = '" + artTypeName + "'"); //NON-NLS
 			BlackboardArtifact.Type type = null;
 			if (rs.next()) {
 				type = new BlackboardArtifact.Type(rs.getInt("artifact_type_id"),
-						rs.getString("type_name"), rs.getString("display_name"));
+						rs.getString("type_name"), rs.getString("display_name"), BlackboardArtifact.Category.fromID(rs.getInt("category_type")));
 				this.typeIdToArtifactTypeMap.put(type.getTypeID(), type);
 				this.typeNameToArtifactTypeMap.put(artTypeName, type);
 			}
@@ -4294,11 +4382,11 @@ BlackboardArtifact.Type getArtifactType(int artTypeId) throws TskCoreException {
 		ResultSet rs = null;
 		try {
 			s = connection.createStatement();
-			rs = connection.executeQuery(s, "SELECT artifact_type_id, type_name, display_name FROM blackboard_artifact_types WHERE artifact_type_id = " + artTypeId + ""); //NON-NLS
+			rs = connection.executeQuery(s, "SELECT artifact_type_id, type_name, display_name, category_type FROM blackboard_artifact_types WHERE artifact_type_id = " + artTypeId + ""); //NON-NLS
 			BlackboardArtifact.Type type = null;
 			if (rs.next()) {
 				type = new BlackboardArtifact.Type(rs.getInt("artifact_type_id"),
-						rs.getString("type_name"), rs.getString("display_name"));
+						rs.getString("type_name"), rs.getString("display_name"), BlackboardArtifact.Category.fromID(rs.getInt("category_type")));
 				this.typeIdToArtifactTypeMap.put(artTypeId, type);
 				this.typeNameToArtifactTypeMap.put(type.getTypeName(), type);
 			}
@@ -4315,6 +4403,8 @@ BlackboardArtifact.Type getArtifactType(int artTypeId) throws TskCoreException {
 
 	/**
 	 * Add an artifact type with the given name. Will return an artifact Type.
+	 * 
+	 * This assumes that the artifact type being added has the category EXTRACTED_DATA.
 	 *
 	 * @param artifactTypeName System (unique) name of artifact
 	 * @param displayName      Display (non-unique) name of artifact
@@ -4326,6 +4416,25 @@ BlackboardArtifact.Type getArtifactType(int artTypeId) throws TskCoreException {
 	 *                          within tsk core
 	 */
 	public BlackboardArtifact.Type addBlackboardArtifactType(String artifactTypeName, String displayName) throws TskCoreException, TskDataException {
+		
+		return addBlackboardArtifactType(artifactTypeName, displayName, BlackboardArtifact.Category.EXTRACTED_DATA);
+	}
+
+	/**
+	 * Add an artifact type with the given name and category. Will return an artifact Type.
+	 *
+	 * @param artifactTypeName System (unique) name of artifact
+	 * @param displayName      Display (non-unique) name of artifact
+	 * @param category		   Artifact type category.
+	 * 
+	 *
+	 * @return Type of the artifact added.
+	 *
+	 * @throws TskCoreException exception thrown if a critical error occurs
+	 * @throws TskDataException exception thrown if given data is already in db
+	 *                          within tsk core
+	 */
+	BlackboardArtifact.Type addBlackboardArtifactType(String artifactTypeName, String displayName, BlackboardArtifact.Category category) throws TskCoreException, TskDataException {
 		CaseDbConnection connection = connections.getConnection();
 		acquireSingleUserCaseWriteLock();
 		Statement s = null;
@@ -4346,8 +4455,8 @@ public BlackboardArtifact.Type addBlackboardArtifactType(String artifactTypeName
 						maxID++;
 					}
 				}
-				connection.executeUpdate(s, "INSERT INTO blackboard_artifact_types (artifact_type_id, type_name, display_name) VALUES ('" + maxID + "', '" + artifactTypeName + "', '" + displayName + "')"); //NON-NLS
-				BlackboardArtifact.Type type = new BlackboardArtifact.Type(maxID, artifactTypeName, displayName);
+				connection.executeUpdate(s, "INSERT INTO blackboard_artifact_types (artifact_type_id, type_name, display_name, category_type) VALUES ('" + maxID + "', '" + artifactTypeName + "', '" + displayName +  "', " + category.getID() + " )"); //NON-NLS
+				BlackboardArtifact.Type type = new BlackboardArtifact.Type(maxID, artifactTypeName, displayName, category);
 				this.typeIdToArtifactTypeMap.put(type.getTypeID(), type);
 				this.typeNameToArtifactTypeMap.put(type.getTypeName(), type);
 				connection.commitTransaction();
@@ -4419,6 +4528,62 @@ public ArrayList<BlackboardAttribute> getBlackboardAttributes(final BlackboardAr
 		}
 	}
 
+	/**
+	 * Get the attributes associated with the given file.
+	 * @param file
+	 * @return
+	 * @throws TskCoreException 
+	 */
+	ArrayList<Attribute> getFileAttributes(final AbstractFile file) throws TskCoreException {
+		CaseDbConnection connection = connections.getConnection();
+		acquireSingleUserCaseReadLock();
+		ResultSet rs = null;
+		try {
+			Statement statement = connection.createStatement();
+			rs = connection.executeQuery(statement, "SELECT attrs.obj_id AS obj_id, "
+					+ "attrs.attribute_type_id AS attribute_type_id, "
+					+ "attrs.value_type AS value_type, attrs.value_byte AS value_byte, "
+					+ "attrs.value_text AS value_text, attrs.value_int32 AS value_int32, "
+					+ "attrs.value_int64 AS value_int64, attrs.value_double AS value_double, "
+					+ "types.type_name AS type_name, types.display_name AS display_name "
+					+ "FROM tsk_file_attributes AS attrs, blackboard_attribute_types AS types WHERE attrs.obj_id = " + file.getId()
+					+ " AND attrs.attribute_type_id = types.attribute_type_id");
+			ArrayList<Attribute> attributes = new ArrayList<Attribute>();
+			while (rs.next()) {
+				int attributeTypeId = rs.getInt("attribute_type_id");
+				String attributeTypeName = rs.getString("type_name");
+				BlackboardAttribute.Type attributeType;
+				if (this.typeIdToAttributeTypeMap.containsKey(attributeTypeId)) {
+					attributeType = this.typeIdToAttributeTypeMap.get(attributeTypeId);
+				} else {
+					attributeType = new BlackboardAttribute.Type(attributeTypeId, attributeTypeName,
+							rs.getString("display_name"),
+							BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.fromType(rs.getInt("value_type")));
+					this.typeIdToAttributeTypeMap.put(attributeTypeId, attributeType);
+					this.typeNameToAttributeTypeMap.put(attributeTypeName, attributeType);
+				}
+
+				final Attribute attr = new Attribute(
+						rs.getLong("obj_id"),
+						attributeType,
+						rs.getInt("value_int32"),
+						rs.getLong("value_int64"),
+						rs.getDouble("value_double"),
+						rs.getString("value_text"),
+						rs.getBytes("value_byte"), this
+				);
+				attributes.add(attr);
+			}
+			return attributes;
+		} catch (SQLException ex) {
+			throw new TskCoreException("Error getting attributes for file, file id = " + file.getId(), ex);
+		} finally {
+			closeResultSet(rs);
+			connection.close();
+			releaseSingleUserCaseReadLock();
+		}
+	}
+
 	/**
 	 * Get all attributes that match a where clause. The clause should begin
 	 * with "WHERE" or "JOIN". To use this method you must know the database
@@ -4549,7 +4714,7 @@ public BlackboardArtifact newBlackboardArtifact(int artifactTypeID, long obj_id)
 	public BlackboardArtifact newBlackboardArtifact(ARTIFACT_TYPE artifactType, long obj_id) throws TskCoreException {
 		return newBlackboardArtifact(artifactType.getTypeID(), obj_id, artifactType.getLabel(), artifactType.getDisplayName());
 	}
-	
+
 	/**
 	 * Add a new blackboard artifact with the given type.
 	 *
@@ -4564,21 +4729,21 @@ public BlackboardArtifact newBlackboardArtifact(ARTIFACT_TYPE artifactType, long
 	 */
 	BlackboardArtifact newBlackboardArtifact(int artifactTypeID, long obj_id, long data_source_obj_id) throws TskCoreException {
 		BlackboardArtifact.Type type = getArtifactType(artifactTypeID);
-		return newBlackboardArtifact(artifactTypeID, obj_id, type.getTypeName(), type.getDisplayName(), data_source_obj_id);
+		try (CaseDbConnection connection = connections.getConnection()) {
+			return newBlackboardArtifact(artifactTypeID, obj_id, type.getTypeName(), type.getDisplayName(), data_source_obj_id, connection);
+		}
 	}
-
+	
 	private BlackboardArtifact newBlackboardArtifact(int artifact_type_id, long obj_id, String artifactTypeName, String artifactDisplayName) throws TskCoreException {
 		try (CaseDbConnection connection = connections.getConnection()) {
 			long data_source_obj_id = getDataSourceObjectId(connection, obj_id);
-			return this.newBlackboardArtifact(artifact_type_id, obj_id, artifactTypeName, artifactDisplayName, data_source_obj_id);
+			return this.newBlackboardArtifact(artifact_type_id, obj_id, artifactTypeName, artifactDisplayName, data_source_obj_id, connection);
 		}
 	}
 
-	private BlackboardArtifact newBlackboardArtifact(int artifact_type_id, long obj_id, String artifactTypeName, String artifactDisplayName, long data_source_obj_id) throws TskCoreException {
-		acquireSingleUserCaseWriteLock();
-		try (CaseDbConnection connection = connections.getConnection()) {
-			long artifact_obj_id = addObject(obj_id, TskData.ObjectType.ARTIFACT.getObjectType(), connection);
-			PreparedStatement statement = null;
+	PreparedStatement createInsertArtifactStatement(int artifact_type_id, long obj_id, long artifact_obj_id,   long data_source_obj_id, CaseDbConnection connection) throws TskCoreException, SQLException {
+	
+			PreparedStatement statement;
 			if (dbType == DbType.POSTGRESQL) {
 				statement = connection.getPreparedStatement(PREPARED_STATEMENT.POSTGRESQL_INSERT_ARTIFACT, Statement.RETURN_GENERATED_KEYS);
 				statement.clearParameters();
@@ -4586,7 +4751,6 @@ private BlackboardArtifact newBlackboardArtifact(int artifact_type_id, long obj_
 				statement.setLong(2, artifact_obj_id);
 				statement.setLong(3, data_source_obj_id);
 				statement.setInt(4, artifact_type_id);
-
 			} else {
 				statement = connection.getPreparedStatement(PREPARED_STATEMENT.INSERT_ARTIFACT, Statement.RETURN_GENERATED_KEYS);
 				statement.clearParameters();
@@ -4596,8 +4760,17 @@ private BlackboardArtifact newBlackboardArtifact(int artifact_type_id, long obj_
 				statement.setLong(3, artifact_obj_id);
 				statement.setLong(4, data_source_obj_id);
 				statement.setInt(5, artifact_type_id);
-
 			}
+		
+		return statement;
+	}
+	
+	BlackboardArtifact newBlackboardArtifact(int artifact_type_id, long obj_id, String artifactTypeName, String artifactDisplayName, long data_source_obj_id, CaseDbConnection connection) throws TskCoreException {
+		acquireSingleUserCaseWriteLock();
+		try  {
+			long artifact_obj_id = addObject(obj_id, TskData.ObjectType.ARTIFACT.getObjectType(), connection);
+			PreparedStatement statement = createInsertArtifactStatement(artifact_type_id, obj_id, artifact_obj_id, data_source_obj_id, connection);
+			
 			connection.executeUpdate(statement);
 			try (ResultSet resultSet = statement.getGeneratedKeys()) {
 				resultSet.next();
@@ -4611,6 +4784,78 @@ private BlackboardArtifact newBlackboardArtifact(int artifact_type_id, long obj_
 		}
 	}
 
+	/**
+	 * Creates a new analysis result by inserting a row in the artifacts table
+	 * and a corresponding row in the tsk_analysis_results table.
+	 *
+	 * @param artifactType       Analysis result artifact type.
+	 * @param obj_id             Object id of parent.
+	 * @param data_source_obj_id Data source object id.
+	 * @param score              Score.
+	 * @param conclusion         Conclusion, may be null or an empty string.
+	 * @param configuration      Configuration used by analysis, may be null or
+	 *                           an empty string.
+	 * @param justification      Justification, may be null or an empty string.
+	 * @param connection         Database connection to use.
+	 *
+	 * @return Analysis result.
+	 *
+	 * @throws TskCoreException
+	 */
+	AnalysisResult newAnalysisResult(BlackboardArtifact.Type artifactType, long obj_id, long data_source_obj_id, Score score, String conclusion, String configuration, String justification, CaseDbConnection connection) throws TskCoreException {
+		
+		if (artifactType.getCategory() != BlackboardArtifact.Category.ANALYSIS_RESULT) {
+			throw new TskCoreException(String.format("Artifact type (name = %s) is not of the AnalysisResult category. ", artifactType.getTypeName()) );
+		}
+		
+		long artifactID;
+		acquireSingleUserCaseWriteLock();
+		try {
+			// add a row in tsk_objects
+			long artifact_obj_id = addObject(obj_id, TskData.ObjectType.ARTIFACT.getObjectType(), connection);
+
+			// add a row in blackboard_artifacts table
+			PreparedStatement insertArtifactstatement;
+			ResultSet resultSet = null;
+			try {
+				insertArtifactstatement = createInsertArtifactStatement(artifactType.getTypeID(), obj_id, artifact_obj_id, data_source_obj_id, connection);
+				connection.executeUpdate(insertArtifactstatement);
+				resultSet = insertArtifactstatement.getGeneratedKeys();
+				resultSet.next();
+				artifactID = resultSet.getLong(1); //last_insert_rowid()
+
+				// add a row in tsk_analysis_results table
+				PreparedStatement analysisResultsStatement;
+
+				analysisResultsStatement = connection.getPreparedStatement(PREPARED_STATEMENT.INSERT_ANALYSIS_RESULT);
+				analysisResultsStatement.clearParameters();
+
+				analysisResultsStatement.setLong(1, artifact_obj_id);
+				analysisResultsStatement.setString(2, (conclusion != null) ? conclusion : "");
+				analysisResultsStatement.setInt(3, score.getSignificance().getId());
+				analysisResultsStatement.setInt(4, score.getConfidence().getId());
+				analysisResultsStatement.setString(5, (configuration != null) ? configuration : "");
+				analysisResultsStatement.setString(6, (justification != null) ? justification : "");
+
+				connection.executeUpdate(analysisResultsStatement);
+
+				return new AnalysisResult(this, artifactID, obj_id, artifact_obj_id, data_source_obj_id, artifactType.getTypeID(),
+						artifactType.getTypeName(), artifactType.getDisplayName(),
+						BlackboardArtifact.ReviewStatus.UNDECIDED, true,
+						score, (conclusion != null) ? conclusion : "",
+						(configuration != null) ? configuration : "", (justification != null) ? justification : "");
+
+			} finally {
+				closeResultSet(resultSet);
+			}
+		
+		} catch (SQLException ex) {
+			throw new TskCoreException("Error creating a analysis result", ex);
+		} finally {
+			releaseSingleUserCaseWriteLock();
+		}
+	}
+	
 	/**
 	 * Checks if the content object has children. Note: this is generally more
 	 * efficient then preloading all children and checking if the set is empty,
@@ -5974,6 +6219,7 @@ public Image addImage(TskData.TSK_IMG_TYPE_ENUM type, long sectorSize, long size
 			preparedStatement.setLong(1, newObjId);
 			preparedStatement.setString(2, deviceId);
 			preparedStatement.setString(3, timezone);
+			preparedStatement.setLong(4, new Date().getTime());
 			connection.executeUpdate(preparedStatement);
 
 			// Create the new Image object
@@ -6220,11 +6466,76 @@ public FsContent addFileSystemFile(long dataSourceObjId, long fsObjId,
 			TSK_FS_NAME_FLAG_ENUM dirFlag, short metaFlags, long size,
 			long ctime, long crtime, long atime, long mtime,
 			boolean isFile, Content parent) throws TskCoreException {
+		
+		CaseDbTransaction transaction = beginTransaction();
+		try {
+
+			FsContent fileSystemFile = addFileSystemFile(dataSourceObjId, fsObjId, fileName,
+					metaAddr, metaSeq, attrType, attrId, dirFlag, metaFlags, size,
+					ctime, crtime, atime, mtime, null, null, null, isFile, parent,
+					Collections.emptyList(), transaction);
+			
+			transaction.commit();
+			transaction = null;
+			return fileSystemFile;
+		} finally {
+			if (null != transaction) {
+				try {
+					transaction.rollback();
+				} catch (TskCoreException ex2) {
+					logger.log(Level.SEVERE, "Failed to rollback transaction after exception", ex2);
+				}
+			}
+		}
+	}
+
+	/**
+	 * Add a file system file.
+	 *
+	 * @param dataSourceObjId The object id of the root data source of this
+	 *                        file.
+	 * @param fsObjId         The file system object id.
+	 * @param fileName        The name of the file.
+	 * @param metaAddr        The meta address of the file.
+	 * @param metaSeq         The meta address sequence of the file.
+	 * @param attrType        The attributed type of the file.
+	 * @param attrId          The attribute id.
+	 * @param dirFlag         The allocated status from the name structure
+	 * @param metaFlags       The allocated status of the file, usually as
+	 *                        reported in the metadata structure of the file
+	 *                        system.
+	 * @param size            The size of the file in bytes.
+	 * @param ctime           The changed time of the file.
+	 * @param crtime          The creation time of the file.
+	 * @param atime           The accessed time of the file
+	 * @param mtime           The modified time of the file.
+	 * @param md5Hash         The MD5 hash of the file
+	 * @param sha256Hash      The SHA256 hash of the file
+	 * @param mimeType        The MIME type of the file
+	 * @param isFile          True, unless the file is a directory.
+	 * @param parent          The parent of the file (e.g., a virtual
+	 *                        directory).
+	 * @param fileAttributes  A list of file attributes. May be empty.
+	 * @param transaction     A caller-managed transaction within which the add
+	 *                        file operations are performed.
+	 *
+	 * @return Newly created file
+	 *
+	 * @throws TskCoreException
+	 */
+	public FsContent addFileSystemFile(long dataSourceObjId, long fsObjId,
+			String fileName,
+			long metaAddr, int metaSeq,
+			TSK_FS_ATTR_TYPE_ENUM attrType, int attrId,
+			TSK_FS_NAME_FLAG_ENUM dirFlag, short metaFlags, long size,
+			long ctime, long crtime, long atime, long mtime,
+			String md5Hash, String sha256Hash, String mimeType,
+			boolean isFile, Content parent, List<Attribute> fileAttributes, CaseDbTransaction transaction) throws TskCoreException {
 
 		TimelineManager timelineManager = getTimelineManager();
 
-		CaseDbTransaction transaction = beginTransaction();
 		Statement queryStatement = null;
+		String parentPath = "/";
 		try {
 			CaseDbConnection connection = transaction.getConnection();
 
@@ -6232,8 +6543,6 @@ public FsContent addFileSystemFile(long dataSourceObjId, long fsObjId,
 			// INSERT INTO tsk_objects (par_obj_id, type) VALUES (?, ?)
 			long objectId = addObject(parent.getId(), TskData.ObjectType.ABSTRACTFILE.getObjectType(), connection);
 
-			String parentPath;
-
 			if (parent instanceof AbstractFile) {
 				AbstractFile parentFile = (AbstractFile) parent;
 				if (isRootDirectory(parentFile, transaction)) {
@@ -6268,40 +6577,38 @@ public FsContent addFileSystemFile(long dataSourceObjId, long fsObjId,
 			statement.setLong(17, crtime);
 			statement.setLong(18, atime);
 			statement.setLong(19, mtime);
-			statement.setString(20, parentPath);
+			statement.setString(20, md5Hash);
+			statement.setString(21, sha256Hash);
+			statement.setString(22, mimeType);
+			statement.setString(23, parentPath);
 			final String extension = extractExtension(fileName);
-			statement.setString(21, extension);
+			statement.setString(24, extension);
 
 			connection.executeUpdate(statement);
 
 			DerivedFile derivedFile = new DerivedFile(this, objectId, dataSourceObjId, fileName, dirType, metaType, dirFlag, metaFlags,
-					size, ctime, crtime, atime, mtime, null, null, null, parentPath, null, parent.getId(), null, null, extension);
+					size, ctime, crtime, atime, mtime, md5Hash, sha256Hash, null, parentPath, null, parent.getId(), mimeType, null, extension);
 
 			timelineManager.addEventsForNewFile(derivedFile, connection);
-
-			transaction.commit();
-			transaction = null;
+			
+			for (Attribute fileAttribute : fileAttributes) {
+				fileAttribute.setAttributeOwnerId(objectId); 
+				fileAttribute.setCaseDatabase(this);
+				addFileAttribute(fileAttribute, connection);
+			}
 
 			return new org.sleuthkit.datamodel.File(this, objectId, dataSourceObjId, fsObjId,
 					attrType, attrId, fileName, metaAddr, metaSeq,
 					dirType, metaType, dirFlag, metaFlags,
 					size, ctime, crtime, atime, mtime,
-					(short) 0, 0, 0, null, null, null, parentPath, null,
-					extension);
-
+					(short) 0, 0, 0, md5Hash, sha256Hash, null, parentPath, mimeType,
+					extension, fileAttributes);
+	
 		} catch (SQLException ex) {
-			logger.log(Level.WARNING, "Failed to add file system file", ex);
+			throw new TskCoreException(String.format("Failed to INSERT file system file %s (%s) with parent id %d in tsk_files table", fileName, parentPath, parent.getId()), ex);
 		} finally {
 			closeStatement(queryStatement);
-			if (null != transaction) {
-				try {
-					transaction.rollback();
-				} catch (TskCoreException ex2) {
-					logger.log(Level.SEVERE, "Failed to rollback transaction after exception", ex2);
-				}
-			}
-		}
-		return null;
+		} 
 	}
 
 	/**
@@ -8617,7 +8924,7 @@ org.sleuthkit.datamodel.File file(ResultSet rs, FileSystem fs) throws SQLExcepti
 				rs.getLong("ctime"), rs.getLong("crtime"), rs.getLong("atime"), rs.getLong("mtime"), //NON-NLS
 				(short) rs.getInt("mode"), rs.getInt("uid"), rs.getInt("gid"), //NON-NLS
 				rs.getString("md5"), rs.getString("sha256"), FileKnown.valueOf(rs.getByte("known")), //NON-NLS
-				rs.getString("parent_path"), rs.getString("mime_type"), rs.getString("extension")); //NON-NLS
+				rs.getString("parent_path"), rs.getString("mime_type"), rs.getString("extension"), Collections.emptyList()); //NON-NLS
 		f.setFileSystem(fs);
 		return f;
 	}
@@ -9189,6 +9496,39 @@ void setImageName(String name, long objId) throws TskCoreException {
 		}
 	}
 
+
+
+	/**
+	 * Updates the image's total size and sector size.This function may be used
+	 * to update the sizes after the image was created.
+	 *
+	 * Can only update the sizes if they were not set before. Will throw
+	 * TskCoreException if the values in the db are not 0 prior to this call.
+	 *
+	 * @param imgage     The image that needs to be updated
+	 * @param totalSize  The total size
+	 * @param sectorSize The sector size
+	 *
+	 * @throws TskCoreException If there is an error updating the case database.
+	 *
+	 */
+	void setImageSizes(Image image, long totalSize, long sectorSize) throws TskCoreException {
+
+		acquireSingleUserCaseWriteLock();
+		try (CaseDbConnection connection = connections.getConnection();) {
+			PreparedStatement preparedStatement = connection.getPreparedStatement(SleuthkitCase.PREPARED_STATEMENT.UPDATE_IMAGE_SIZES);
+			preparedStatement.clearParameters();
+			preparedStatement.setLong(1, totalSize);
+			preparedStatement.setLong(2, sectorSize);
+			preparedStatement.setLong(3, image.getId());
+			connection.executeUpdate(preparedStatement);
+		} catch (SQLException ex) {
+			throw new TskCoreException(String.format("Error updating image sizes to %d and sector size to %d for object ID %d ",totalSize, sectorSize, image.getId()), ex);
+		} finally {
+			releaseSingleUserCaseWriteLock();
+		}
+	}
+
 	/**
 	 * Stores the MIME type of a file in the case database and updates the MIME
 	 * type of the given file object.
@@ -9467,6 +9807,39 @@ void setAcquisitionDetails(DataSource datasource, String details) throws TskCore
 		}
 	}
 
+
+	/**
+	 * Sets the acquisition tool details such as its name, version number and
+	 * any settings used during the acquisition to acquire data.
+	 *
+	 * @param datasource The datasource object
+	 * @param name       The name of the acquisition tool. May be NULL.
+	 * @param version    The acquisition tool version number. May be NULL.
+	 * @param settings   The settings used by the acquisition tool. May be NULL.
+	 *
+	 * @throws TskCoreException Thrown if the database write fails
+	 */
+	void setAcquisitionToolDetails(DataSource datasource, String name, String version, String settings) throws TskCoreException {
+
+		long id = datasource.getId();
+		CaseDbConnection connection = connections.getConnection();
+		acquireSingleUserCaseWriteLock();
+		try {
+			PreparedStatement statement = connection.getPreparedStatement(PREPARED_STATEMENT.UPDATE_ACQUISITION_TOOL_SETTINGS);
+			statement.clearParameters();
+			statement.setString(1, settings);
+			statement.setString(2, name);
+			statement.setString(3, version);
+			statement.setLong(4, id);
+			connection.executeUpdate(statement);
+		} catch (SQLException ex) {
+			throw new TskCoreException("Error setting acquisition details", ex);
+		} finally {
+			connection.close();
+			releaseSingleUserCaseWriteLock();
+		}
+	}
+
 	/**
 	 * Set the acquisition details in the data_source_info table.
 	 * 
@@ -9525,6 +9898,71 @@ String getAcquisitionDetails(DataSource datasource) throws TskCoreException {
 		}
 	}
 
+	/**
+	 * Get String value from the provided column from data_source_info table. 
+	 * 
+	 * @param datasource The datasource
+	 * @param columnName The column from which the data should be returned 
+	 * @return String value from the column 
+	 * @throws TskCoreException 
+	 */
+	String getDataSourceInfoString(DataSource datasource, String columnName) throws TskCoreException {
+		long id = datasource.getId();
+		CaseDbConnection connection = connections.getConnection();
+		acquireSingleUserCaseReadLock();
+		ResultSet rs = null;
+		String returnValue = "";
+		try {
+			PreparedStatement statement = connection.getPreparedStatement(PREPARED_STATEMENT.SELECT_ACQUISITION_TOOL_SETTINGS);
+			statement.clearParameters();
+			statement.setLong(1, id);
+			rs = connection.executeQuery(statement);
+			if (rs.next()) {
+				returnValue = rs.getString(columnName);
+			}
+			return returnValue;
+		} catch (SQLException ex) {
+			throw new TskCoreException("Error setting acquisition details", ex);
+		} finally {
+			closeResultSet(rs);
+			connection.close();
+			releaseSingleUserCaseReadLock();
+		}
+	}
+
+
+	/**
+	 * Get Long value from the provided column from data_source_info table.
+	 *
+	 * @param datasource The datasource
+	 * @param columnName The column from which the data should be returned
+	 * @return Long value from the column
+	 * @throws TskCoreException
+	 */
+	Long getDataSourceInfoLong(DataSource datasource, String columnName) throws TskCoreException {
+		long id = datasource.getId();
+		CaseDbConnection connection = connections.getConnection();
+		acquireSingleUserCaseReadLock();
+		ResultSet rs = null;
+		Long returnValue = null;
+		try {
+			PreparedStatement statement = connection.getPreparedStatement(PREPARED_STATEMENT.SELECT_ACQUISITION_TOOL_SETTINGS);
+			statement.clearParameters();
+			statement.setLong(1, id);
+			rs = connection.executeQuery(statement);
+			if (rs.next()) {
+				returnValue = rs.getLong(columnName);
+			}
+			return returnValue;
+		} catch (SQLException ex) {
+			throw new TskCoreException("Error setting acquisition details", ex);
+		} finally {
+			closeResultSet(rs);
+			connection.close();
+			releaseSingleUserCaseReadLock();
+		}
+	}
+
 	/**
 	 * Set the review status of the given artifact to newStatus
 	 *
@@ -11174,6 +11612,8 @@ private enum PREPARED_STATEMENT {
 				+ "VALUES (?, ?, ?, ?, ?," + BlackboardArtifact.ReviewStatus.UNDECIDED.getID() + ")"), //NON-NLS
 		POSTGRESQL_INSERT_ARTIFACT("INSERT INTO blackboard_artifacts (artifact_id, obj_id, artifact_obj_id, data_source_obj_id, artifact_type_id, review_status_id) " //NON-NLS
 				+ "VALUES (DEFAULT, ?, ?, ?, ?," + BlackboardArtifact.ReviewStatus.UNDECIDED.getID() + ")"), //NON-NLS
+		INSERT_ANALYSIS_RESULT("INSERT INTO tsk_analysis_results (obj_id, conclusion, significance, confidence, configuration, justification) " //NON-NLS
+				+ "VALUES (?, ?, ?, ?, ?, ?)"), //NON-NLS
 		INSERT_STRING_ATTRIBUTE("INSERT INTO blackboard_attributes (artifact_id, artifact_type_id, source, context, attribute_type_id, value_type, value_text) " //NON-NLS
 				+ "VALUES (?,?,?,?,?,?,?)"), //NON-NLS
 		INSERT_BYTE_ATTRIBUTE("INSERT INTO blackboard_attributes (artifact_id, artifact_type_id, source, context, attribute_type_id, value_type, value_byte) " //NON-NLS
@@ -11184,6 +11624,8 @@ private enum PREPARED_STATEMENT {
 				+ "VALUES (?,?,?,?,?,?,?)"), //NON-NLS
 		INSERT_DOUBLE_ATTRIBUTE("INSERT INTO blackboard_attributes (artifact_id, artifact_type_id, source, context, attribute_type_id, value_type, value_double) " //NON-NLS
 				+ "VALUES (?,?,?,?,?,?,?)"), //NON-NLS
+		INSERT_FILE_ATTRIBUTE("INSERT INTO tsk_file_attributes (obj_id, attribute_type_id, value_type, value_byte, value_text, value_int32, value_int64, value_double) " //NON-NLS
+				+ "VALUES (?,?,?,?,?,?,?,?)"), //NON-NLS
 		SELECT_FILES_BY_DATA_SOURCE_AND_NAME("SELECT * FROM tsk_files WHERE LOWER(name) LIKE LOWER(?) AND LOWER(name) NOT LIKE LOWER('%journal%') AND data_source_obj_id = ?"), //NON-NLS
 		SELECT_FILES_BY_DATA_SOURCE_AND_PARENT_PATH_AND_NAME("SELECT * FROM tsk_files WHERE LOWER(name) LIKE LOWER(?) AND LOWER(name) NOT LIKE LOWER('%journal%') AND LOWER(parent_path) LIKE LOWER(?) AND data_source_obj_id = ?"), //NON-NLS
 		UPDATE_FILE_MD5("UPDATE tsk_files SET md5 = ? WHERE obj_id = ?"), //NON-NLS
@@ -11194,7 +11636,9 @@ private enum PREPARED_STATEMENT {
 		SELECT_IMAGE_SHA1("SELECT sha1 FROM tsk_image_info WHERE obj_id = ?"), //NON-NLS
 		SELECT_IMAGE_SHA256("SELECT sha256 FROM tsk_image_info WHERE obj_id = ?"), //NON-NLS
 		UPDATE_ACQUISITION_DETAILS("UPDATE data_source_info SET acquisition_details = ? WHERE obj_id = ?"), //NON-NLS
+		UPDATE_ACQUISITION_TOOL_SETTINGS("UPDATE data_source_info SET acquisition_tool_settings = ?, acquisition_tool_name = ?, acquisition_tool_version = ? WHERE obj_id = ?"), //NON-NLS
 		SELECT_ACQUISITION_DETAILS("SELECT acquisition_details FROM data_source_info WHERE obj_id = ?"), //NON-NLS
+		SELECT_ACQUISITION_TOOL_SETTINGS("SELECT acquisition_tool_settings, acquisition_tool_name, acquisition_tool_version, added_date_time FROM data_source_info WHERE obj_id = ?"), //NON-NLS
 		SELECT_LOCAL_PATH_FOR_FILE("SELECT path FROM tsk_files_path WHERE obj_id = ?"), //NON-NLS
 		SELECT_ENCODING_FOR_FILE("SELECT encoding_type FROM tsk_files_path WHERE obj_id = ?"), // NON-NLS
 		SELECT_LOCAL_PATH_AND_ENCODING_FOR_FILE("SELECT path, encoding_type FROM tsk_files_path WHERE obj_id = ?"), // NON_NLS
@@ -11206,8 +11650,8 @@ private enum PREPARED_STATEMENT {
 		INSERT_OBJECT("INSERT INTO tsk_objects (par_obj_id, type) VALUES (?, ?)"), //NON-NLS
 		INSERT_FILE("INSERT INTO tsk_files (obj_id, fs_obj_id, name, type, has_path, dir_type, meta_type, dir_flags, meta_flags, size, ctime, crtime, atime, mtime, md5, sha256, known, mime_type, parent_path, data_source_obj_id,extension) " //NON-NLS
 				+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"), //NON-NLS
-		INSERT_FILE_SYSTEM_FILE("INSERT INTO tsk_files(obj_id, fs_obj_id, data_source_obj_id, attr_type, attr_id, name, meta_addr, meta_seq, type, has_path, dir_type, meta_type, dir_flags, meta_flags, size, ctime, crtime, atime, mtime, parent_path, extension)"
-				+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"), // NON-NLS
+		INSERT_FILE_SYSTEM_FILE("INSERT INTO tsk_files(obj_id, fs_obj_id, data_source_obj_id, attr_type, attr_id, name, meta_addr, meta_seq, type, has_path, dir_type, meta_type, dir_flags, meta_flags, size, ctime, crtime, atime, mtime, md5, sha256, mime_type, parent_path, extension)"
+				+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"), // NON-NLS
 		UPDATE_DERIVED_FILE("UPDATE tsk_files SET type = ?, dir_type = ?, meta_type = ?, dir_flags = ?,  meta_flags = ?, size= ?, ctime= ?, crtime= ?, atime= ?, mtime= ?, mime_type = ?  "
 				+ "WHERE obj_id = ?"), //NON-NLS
 		INSERT_LAYOUT_FILE("INSERT INTO tsk_file_layout (obj_id, byte_start, byte_len, sequence) " //NON-NLS
@@ -11317,11 +11761,12 @@ private enum PREPARED_STATEMENT {
 		INSERT_EXAMINER_SQLITE("INSERT OR IGNORE INTO tsk_examiners (login_name) VALUES (?)"),
 		UPDATE_FILE_NAME("UPDATE tsk_files SET name = ? WHERE obj_id = ?"),
 		UPDATE_IMAGE_NAME("UPDATE tsk_image_info SET display_name = ? WHERE obj_id = ?"),
+		UPDATE_IMAGE_SIZES("UPDATE tsk_image_info SET size = ?, ssize = ? WHERE obj_id = ?"),
 		DELETE_IMAGE_NAME("DELETE FROM tsk_image_names WHERE obj_id = ?"),
 		INSERT_IMAGE_NAME("INSERT INTO tsk_image_names (obj_id, name, sequence) VALUES (?, ?, ?)"),
 		INSERT_IMAGE_INFO("INSERT INTO tsk_image_info (obj_id, type, ssize, tzone, size, md5, sha1, sha256, display_name)"
 				+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"),
-		INSERT_DATA_SOURCE_INFO("INSERT INTO data_source_info (obj_id, device_id, time_zone) VALUES (?, ?, ?)"),
+		INSERT_DATA_SOURCE_INFO("INSERT INTO data_source_info (obj_id, device_id, time_zone, added_date_time) VALUES (?, ?, ?, ?)"),
 		INSERT_VS_INFO("INSERT INTO tsk_vs_info (obj_id, vs_type, img_offset, block_size) VALUES (?, ?, ?, ?)"),
 		INSERT_VS_PART_SQLITE("INSERT INTO tsk_vs_parts (obj_id, addr, start, length, desc, flags) VALUES (?, ?, ?, ?, ?, ?)"),
 		INSERT_VS_PART_POSTGRESQL("INSERT INTO tsk_vs_parts (obj_id, addr, start, length, descr, flags) VALUES (?, ?, ?, ?, ?, ?)"),
@@ -11931,6 +12376,11 @@ public static final class CaseDbTransaction {
 		private final CaseDbConnection connection;
 		private SleuthkitCase sleuthkitCase;
 
+		// A collection of object score changes that ocuured as part of this transaction.
+		// When the transaction is committed, events are fired to notify any listeners.
+		// Score changes are stored as a map keyed by objId to prevent duplicates.
+		private Map<Long, ScoreChange> scoreChangeMap = new HashMap<>(); 
+		
 		private CaseDbTransaction(SleuthkitCase sleuthkitCase, CaseDbConnection connection) throws TskCoreException {
 			this.connection = connection;
 			this.sleuthkitCase = sleuthkitCase;
@@ -11953,6 +12403,16 @@ CaseDbConnection getConnection() {
 			return this.connection;
 		}
 
+		
+		/**
+		 * Saves a score change done as part of the transaction.
+		 * 
+		 * @param scoreChange Score change.
+		 */
+		void registerScoreChange(ScoreChange scoreChange) {
+			scoreChangeMap.put(scoreChange.getObjId(), scoreChange);
+		}
+		
 		/**
 		 * Commits the transaction on the case database that was begun when this
 		 * object was constructed.
@@ -11962,6 +12422,18 @@ CaseDbConnection getConnection() {
 		public void commit() throws TskCoreException {
 			try {
 				this.connection.commitTransaction();
+
+				if (!scoreChangeMap.isEmpty()) {
+					// Group the score changes by data source id
+					Map<Long, List<ScoreChange>> changesByDataSource = scoreChangeMap.values().stream()
+							.collect(Collectors.groupingBy(ScoreChange::getDataSourceObjectId));
+
+					// Fire an event for each data source with a list of score changes.
+					for (Map.Entry<Long, List<ScoreChange>> entry : changesByDataSource.entrySet()) {
+						sleuthkitCase.fireTSKEvent(new AggregateScoresChangedEvent(entry.getKey(), ImmutableSet.copyOf(entry.getValue())));
+					}
+				}
+
 			} catch (SQLException ex) {
 				throw new TskCoreException("Failed to commit transaction on case database", ex);
 			} finally {
diff --git a/bindings/java/src/org/sleuthkit/datamodel/SpecialDirectory.java b/bindings/java/src/org/sleuthkit/datamodel/SpecialDirectory.java
index 5db5888d5b34d8a0ebbd926d18b1fa52559c2512..ba23c440898deedc491bf1f33e6eeb4bcaeb4931 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/SpecialDirectory.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/SpecialDirectory.java
@@ -48,7 +48,7 @@ public abstract class SpecialDirectory extends AbstractFile {
 			String mimeType) {
 		super(db, objId, dataSourceObjectId, attrType, attrId, name,
 				fileType, metaAddr, metaSeq, dirType, metaType, dirFlag,
-				metaFlags, size, ctime, crtime, atime, mtime, modes, uid, gid, md5Hash, sha256Hash, knownState, parentPath, mimeType, null);
+				metaFlags, size, ctime, crtime, atime, mtime, modes, uid, gid, md5Hash, sha256Hash, knownState, parentPath, mimeType, null, Collections.emptyList());
 	}
 
 	/**
diff --git a/bindings/java/src/org/sleuthkit/datamodel/TskDataSourceEvent.java b/bindings/java/src/org/sleuthkit/datamodel/TskDataSourceEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..e9786bd301fdc81df0acf7a70f9dd72a573e2280
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/TskDataSourceEvent.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+/**
+ *
+ * Defines an interface implemented by data source specific events published by
+ * Sleuthkit. These events are applicable to single data source.
+ */
+public interface TskDataSourceEvent {
+
+	/**
+	 * Returns the object id of the data source that the event pertains to.
+	 *
+	 * All data in an event should pertain to a single data source.
+	 *
+	 * @return Data source object id.
+	 */
+	public long getDataSourceId();
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/TskEvent.java b/bindings/java/src/org/sleuthkit/datamodel/TskEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..089daf46d6b8749b5a050da32c6a9c3286527750
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/TskEvent.java
@@ -0,0 +1,27 @@
+/*
+ * 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;
+
+/**
+ *
+ * A marker interface for events published by SleuthKit. 
+ */
+public interface TskEvent {
+	
+}
diff --git a/bindings/java/test/org/sleuthkit/datamodel/AttributeTest.java b/bindings/java/test/org/sleuthkit/datamodel/AttributeTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..ea8ae30e31549d92d7875245aafde716aee5bd22
--- /dev/null
+++ b/bindings/java/test/org/sleuthkit/datamodel/AttributeTest.java
@@ -0,0 +1,148 @@
+/*
+ * Sleuth Kit Data Model
+ *
+ * Copyright 2021 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 java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.junit.After;
+import org.junit.AfterClass;
+import static org.junit.Assert.assertEquals;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ *
+ * Tests TSK Attribute apis.
+ * Under test are the following
+ * - File Attribute
+ *
+ */
+public class AttributeTest {
+
+	private static final Logger LOGGER = Logger.getLogger(AttributeTest.class.getName());
+
+	private static SleuthkitCase caseDB;
+
+	private final static String TEST_DB = "AttributeApiTest.db";
+
+
+	private static String dbPath = null;
+	private static FileSystem fs = null;
+
+	public AttributeTest (){
+
+	}
+
+	@BeforeClass
+	public static void setUpClass() {
+		String tempDirPath = System.getProperty("java.io.tmpdir");
+		try {
+			dbPath = Paths.get(tempDirPath, TEST_DB).toString();
+
+			// Delete the DB file, in case
+			java.io.File dbFile = new java.io.File(dbPath);
+			dbFile.delete();
+			if (dbFile.getParentFile() != null) {
+				dbFile.getParentFile().mkdirs();
+			}
+
+			// Create new case db
+			caseDB = SleuthkitCase.newCase(dbPath);
+
+			SleuthkitCase.CaseDbTransaction trans = caseDB.beginTransaction();
+
+			Image img = caseDB.addImage(TskData.TSK_IMG_TYPE_ENUM.TSK_IMG_TYPE_DETECT, 512, 1024, "", Collections.emptyList(), "America/NewYork", null, null, null, "first", trans);
+
+			fs = caseDB.addFileSystem(img.getId(), 0, TskData.TSK_FS_TYPE_ENUM.TSK_FS_TYPE_RAW, 0, 0, 0, 0, 0, "", trans);
+
+			trans.commit();
+
+
+			System.out.println("Attributes Test DB created at: " + dbPath);
+		} catch (TskCoreException ex) {
+			LOGGER.log(Level.SEVERE, "Failed to create new case", ex);
+		}
+	}
+
+
+	@AfterClass
+	public static void tearDownClass() {
+
+	}
+
+	@Before
+	public void setUp() {
+	}
+
+	@After
+	public void tearDown() {
+	}
+
+	@Test
+	public void fileAttributeTests() throws TskCoreException {
+
+		String testMD5 = "c67017ead6356b987b30536d35e8f562";
+		List<Attribute> fileAttributes = new ArrayList<>();
+		fileAttributes.add(new Attribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_ACCESSED), 1611233915l));
+
+		List<Attribute> fileAttributes2 = new ArrayList<>();
+		fileAttributes2.add(new Attribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SSID), "S-1-15-3443-2233"));
+
+
+		SleuthkitCase.CaseDbTransaction trans = caseDB.beginTransaction();
+
+
+		FsContent root = caseDB.addFileSystemFile(fs.getDataSource().getId(), fs.getId(), "", 0, 0,
+				TskData.TSK_FS_ATTR_TYPE_ENUM.TSK_FS_ATTR_TYPE_DEFAULT, 0, TskData.TSK_FS_NAME_FLAG_ENUM.ALLOC,
+				(short) 0, 200, 0, 0, 0, 0, null, null, null, false, fs, Collections.emptyList(), trans);
+
+
+		FsContent windows = caseDB.addFileSystemFile(fs.getDataSource().getId(), fs.getId(), "Windows", 0, 0,
+				TskData.TSK_FS_ATTR_TYPE_ENUM.TSK_FS_ATTR_TYPE_DEFAULT, 0, TskData.TSK_FS_NAME_FLAG_ENUM.ALLOC,
+				(short) 0, 200, 0, 0, 0, 0, null, null, null, false, root, Collections.emptyList(), trans);
+		
+		FsContent dllhosts = caseDB.addFileSystemFile(fs.getDataSource().getId(), fs.getId(), "dllhosts.exe", 0, 0,
+				TskData.TSK_FS_ATTR_TYPE_ENUM.TSK_FS_ATTR_TYPE_DEFAULT, 0, TskData.TSK_FS_NAME_FLAG_ENUM.ALLOC,
+				(short)0, 200, 0, 0, 0, 0, testMD5, null, "Applicatione/Exe" , true, windows, fileAttributes, trans);
+
+		FsContent _nofile = caseDB.addFileSystemFile(fs.getDataSource().getId(), fs.getId(), "nofile.exe", 0, 0,
+				TskData.TSK_FS_ATTR_TYPE_ENUM.TSK_FS_ATTR_TYPE_DEFAULT, 0, TskData.TSK_FS_NAME_FLAG_ENUM.ALLOC,
+				(short)0, 200, 0, 0, 0, 0, null, null, "Applicatione/Exe" , true, windows, Collections.emptyList(), trans);
+
+		dllhosts.addAttributes(fileAttributes2, trans);
+		trans.commit();
+ 
+		assertEquals(2, dllhosts.getAttributes().size());
+
+		List<AbstractFile> matchingFiles = caseDB.findFilesByMd5(testMD5);
+		assertEquals(1, matchingFiles.size());
+		assertEquals(2, matchingFiles.get(0).getAttributes().size());
+
+		List<AbstractFile> nofile = caseDB.findFiles(fs.getDataSource(), "nofile.exe");
+		assertEquals(1, nofile.size());
+		assertEquals(0, nofile.get(0).getAttributes().size());
+
+
+	}
+}
diff --git a/bindings/java/test/org/sleuthkit/datamodel/DataModelTestSuite.java b/bindings/java/test/org/sleuthkit/datamodel/DataModelTestSuite.java
index 2ec2da48f63072d83262cf107fb7c1375888d298..8a8826e7df5cb85c2edc7485151b8d20671e8ec6 100644
--- a/bindings/java/test/org/sleuthkit/datamodel/DataModelTestSuite.java
+++ b/bindings/java/test/org/sleuthkit/datamodel/DataModelTestSuite.java
@@ -45,6 +45,7 @@
 @Suite.SuiteClasses({ 
 	CommunicationsManagerTest.class, 
 	CaseDbSchemaVersionNumberTest.class,
+	AttributeTest.class,
 
 //  Note: these tests have dependencies on images being placed in the input folder: nps-2009-canon2-gen6, ntfs1-gen, and small2	
 //	org.sleuthkit.datamodel.TopDownTraversal.class,