diff --git a/bindings/java/doxygen/artifact_catalog.dox b/bindings/java/doxygen/artifact_catalog.dox
index 1f50c7c1ed0ace2df7b38ff0a638458a02a092fd..d27ce4a1de69effa16db8514653793981222fb9d 100644
--- a/bindings/java/doxygen/artifact_catalog.dox
+++ b/bindings/java/doxygen/artifact_catalog.dox
@@ -422,7 +422,16 @@ General metadata for some content.
 ### REQUIRED ATTRIBUTES
 None
 
-
+### OPTIONAL ATTRIBUTES
+- TSK_DATETIME_CREATED  (Timestamp the document was created)
+- TSK_DATETIME_MODIFIED (Timestamp the document was modified)
+- TSK_DESCRIPTION (Title of the document)
+- TSK_LAST_PRINTED_DATETIME (Timestamp when document was last printed)
+- TSK_ORGANIZATION (Organization/Company who owns the document)
+- TSK_OWNER (Author of the document)
+- TSK_PROG_NAME (Program used to create the document)
+- TSK_USER_ID (Last author of the document)
+- TSK_VERSION (Version number of the program used to create the document)
 
 ---
 ## TSK_METADATA_EXIF
diff --git a/bindings/java/jni/auto_db_java.cpp b/bindings/java/jni/auto_db_java.cpp
index 5dd64a30dd7c894c4e5e6688be7ff1de06d777e5..327d5d3602cbdcc938f6f2f52e1165136f8e4a74 100644
--- a/bindings/java/jni/auto_db_java.cpp
+++ b/bindings/java/jni/auto_db_java.cpp
@@ -84,11 +84,6 @@ TskAutoDbJava::initializeJni(JNIEnv * jniEnv, jobject jobj) {
         return TSK_ERR;
     }
 
-    m_addImageNameMethodID = m_jniEnv->GetMethodID(m_callbackClass, "addImageName", "(JLjava/lang/String;J)I");
-    if (m_addImageNameMethodID == NULL) {
-        return TSK_ERR;
-    }
-
     m_addVolumeSystemMethodID = m_jniEnv->GetMethodID(m_callbackClass, "addVsInfo", "(JIJJ)J");
     if (m_addVolumeSystemMethodID == NULL) {
         return TSK_ERR;
@@ -227,29 +222,6 @@ TskAutoDbJava::addImageInfo(int type, TSK_OFF_T ssize, int64_t & objId, const st
     return TSK_OK;
 }
 
-/**
-* Adds one image name
-* @param objId    The object ID of the image
-* @param imgName  The image name
-* @param sequence The sequence number for this image name
-* @returns TSK_ERR on error, TSK_OK on success
-*/
-TSK_RETVAL_ENUM
-TskAutoDbJava::addImageName(int64_t objId, char const* imgName, int sequence) {
-
-    jstring imgNamej = m_jniEnv->NewStringUTF(imgName);
-
-    jint res = m_jniEnv->CallIntMethod(m_javaDbObj, m_addImageNameMethodID,
-        objId, imgNamej, (int64_t)sequence);
-
-    if (res == 0) {
-        return TSK_OK;
-    }
-    else {
-        return TSK_ERR;
-    }
-}
-
 /**
 * Adds volume system to database. Object ID for new vs stored in objId.
 *
diff --git a/bindings/java/src/org/sleuthkit/datamodel/BlackboardAttribute.java b/bindings/java/src/org/sleuthkit/datamodel/BlackboardAttribute.java
index fe9caf98ab67f00ceb6fcb6baef72173162a160f..0e9d449e28ccab2b4ccd05c739a118ff2c6cf3aa 100755
--- a/bindings/java/src/org/sleuthkit/datamodel/BlackboardAttribute.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/BlackboardAttribute.java
@@ -1411,7 +1411,12 @@ public enum ATTRIBUTE_TYPE {
 		
 		TSK_BYTES_RECEIVED(148, "TSK_BYTES_RECEIVED",
 	        bundle.getString("BlackboardAttribute.tskbytesreceived.text"),
-	        TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.LONG)
+	        TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.LONG),
+		
+		TSK_LAST_PRINTED_DATETIME(149, "TSK_LAST_PRINTED_DATETIME",
+	        bundle.getString("BlackboardAttribute.tsklastprinteddatetime.text"),
+	        TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DATETIME),
+		
 		
 		;
 
diff --git a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties
index 8e4eb394218156142eda7c22a938ef892ce76a31..16335ea7bdca9678b386461b6e9db36b52ef82ad 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties
+++ b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties
@@ -198,6 +198,7 @@ BlackboardAttribute.tskdistancefromhome.text=Distance from Homepoint
 BlackboardAttribute.tskhashphotodna.text=PhotoDNA Hash
 BlackboardAttribute.tskbytessent.text=Bytes Sent
 BlackboardAttribute.tskbytesreceived.text=Bytes Received
+BlackboardAttribute.tsklastprinteddatetime.text=Last Printed Date
 AbstractFile.readLocal.exception.msg4.text=Error reading local file\: {0}
 AbstractFile.readLocal.exception.msg1.text=Error reading local file, local path is not set
 AbstractFile.readLocal.exception.msg2.text=Error reading local file, it does not exist at local path\: {0}
@@ -323,6 +324,9 @@ MiscTypes.GPSBookmark.name=GPS Bookmark
 MiscTypes.GPSLastknown.name=GPS Last Known Location
 MiscTypes.GPSearch.name=GPS Search
 MiscTypes.GPSTrack.name=GPS Track
+MiscTypes.metadataLastPrinted.name=Document Last Printed
+MiscTypes.metadataLastSaved.name=Document Last Saved
+MiscTypes.metadataCreated.name=Document Created
 RootEventType.eventTypes.name=Event Types
 WebTypes.webDownloads.name=Web Downloads
 WebTypes.webCookies.name=Web Cookies
diff --git a/bindings/java/src/org/sleuthkit/datamodel/CommunicationsManager.java b/bindings/java/src/org/sleuthkit/datamodel/CommunicationsManager.java
index 8790b4bcaf3a3f29625489ba69ab318a2b3c1b6c..43256938eb117da8235ba6bed79571f74a2de43a 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/CommunicationsManager.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/CommunicationsManager.java
@@ -1291,6 +1291,64 @@ public List<Account.Type> getAccountTypesInUse() throws TskCoreException {
 		}
 	}
 
+	/**
+	 * Gets a list of accounts that are related to the given artifact.
+	 *
+	 * @param artifact
+	 *
+	 * @return A list of distinct accounts or an empty list if none where found.
+	 *
+	 * @throws TskCoreException
+	 */
+	public List<Account> getAccountsRelatedToArtifact(BlackboardArtifact artifact) throws TskCoreException {
+		if (artifact == null) {
+			throw new IllegalArgumentException("null arugment passed to getAccountsRelatedToArtifact");
+		}
+
+		List<Account> accountList = new ArrayList<>();
+		try (CaseDbConnection connection = db.getConnection()) {
+			db.acquireSingleUserCaseReadLock();
+			try {
+				// In order to get a list of all the unique accounts in a relationship with the given aritfact
+				// we must first union a list of the unique account1_id in the relationship with artifact
+				// then the unique account2_id (inner select with union).  The outter select assures the list
+				// of the inner select only contains unique accounts.
+				String query = String.format("SELECT DISTINCT (account_id), account_type_id, account_unique_identifier"
+						+ "	FROM ("
+						+ " SELECT DISTINCT (account_id), account_type_id, account_unique_identifier"
+						+ " FROM accounts"
+						+ " JOIN account_relationships ON account1_id = account_id"
+						+ " WHERE relationship_source_obj_id = %d"
+						+ " UNION "
+						+ " SELECT DISTINCT (account_id), account_type_id, account_unique_identifier"
+						+ " FROM accounts"
+						+ " JOIN account_relationships ON account2_id = account_id"
+						+ " WHERE relationship_source_obj_id = %d)", artifact.getId(), artifact.getId());
+				try (Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery(query)) {
+					while (rs.next()) {
+						Account.Type accountType = null;
+						int accountTypeId = rs.getInt("account_type_id");
+						for (Map.Entry<Account.Type, Integer> entry : accountTypeToTypeIdMap.entrySet()) {
+							if (entry.getValue() == accountTypeId) {
+								accountType = entry.getKey();
+								break;
+							}
+						}
+
+						accountList.add(new Account(rs.getInt("account_id"), accountType, rs.getString("account_unique_identifier")));
+					}
+				} catch (SQLException ex) {
+					throw new TskCoreException("Unable to get account list for give artifact " + artifact.getId(), ex);
+				}
+
+			} finally {
+				db.releaseSingleUserCaseReadLock();
+			}
+		}
+
+		return accountList;
+	}
+
 	/**
 	 * Get account_type_id for the given account type.
 	 *
@@ -1327,8 +1385,6 @@ private String normalizeAccountID(Account.Type accountType, String accountUnique
 		return normailzeAccountID;
 	}
 
-	
-
 	/**
 	 * Builds the SQL for the given CommunicationsFilter.
 	 *
diff --git a/bindings/java/src/org/sleuthkit/datamodel/Image.java b/bindings/java/src/org/sleuthkit/datamodel/Image.java
index 1b920333abeb0c7a6ab813d46f4c107e0b455b5c..a465af5975966e2f9cc12870732f1af7f1a3328c 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/Image.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/Image.java
@@ -259,8 +259,8 @@ public List<Volume> getVolumes() throws TskCoreException {
 	 * @throws TskCoreException
 	 */
 	public List<FileSystem> getFileSystems() throws TskCoreException {
-		List<FileSystem> fs = new ArrayList<FileSystem>();
-		fs.addAll(getSleuthkitCase().getFileSystems(this));
+		List<FileSystem> fs = new ArrayList<>();
+		fs.addAll(getSleuthkitCase().getImageFileSystems(this));
 		return fs;
 	}
 
diff --git a/bindings/java/src/org/sleuthkit/datamodel/JniDbHelper.java b/bindings/java/src/org/sleuthkit/datamodel/JniDbHelper.java
index 5d61e57c7d66e18cf876584eb7f58e0b99c89e00..0422de29a3774ebc864b896ea03dd692a6696941 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/JniDbHelper.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/JniDbHelper.java
@@ -22,8 +22,10 @@
 import java.util.List;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.LinkedList;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Queue;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import org.sleuthkit.datamodel.SleuthkitCase.CaseDbTransaction;
@@ -33,6 +35,9 @@
  * case database. All callbacks from the native code should come through this class.
  * Any changes to the method signatures in this class will require changes to the 
  * native code.
+ * 
+ * Note that this code should only be used for the add image process, and not
+ * to add additional files afterward.
  */
 class JniDbHelper {
     
@@ -47,8 +52,9 @@ class JniDbHelper {
     private final Map<ParentCacheKey, Long> parentDirCache = new HashMap<>();
     
     private static final long BATCH_FILE_THRESHOLD = 500;
-    private final List<FileInfo> batchedFiles = new ArrayList<>();
-    private final List<LayoutRangeInfo> batchedLayoutRanges = new ArrayList<>();
+    private final Queue<FileInfo> batchedFiles = new LinkedList<>();
+    private final Queue<LayoutRangeInfo> batchedLayoutRanges = new LinkedList<>();
+    private final List<Long> layoutFileIds = new ArrayList<>();
     
     JniDbHelper(SleuthkitCase caseDb, AddDataSourceCallbacks addDataSourceCallbacks) {
         this.caseDb = caseDb;
@@ -95,6 +101,7 @@ private void revertTransaction() {
     void finish() {
         addBatchedFilesToDb();
         addBatchedLayoutRangesToDb();
+        processLayoutFiles();
     }
     
     /**
@@ -125,7 +132,13 @@ long addImageInfo(int type, long ssize, String timezone,
             }
             commitTransaction();
             
-			addDataSourceCallbacks.onDataSourceAdded(objId);
+			try {
+				addDataSourceCallbacks.onDataSourceAdded(objId);
+			} catch (Exception ex) {
+				// Exception firewall - we do not want to return to the native code without
+				// passing it the data source ID
+				logger.log(Level.SEVERE, "Unexpected error from data source added callback", ex);
+			}
             return objId;
         } catch (TskCoreException ex) {
             logger.log(Level.SEVERE, "Error adding image to the database", ex);
@@ -134,30 +147,6 @@ long addImageInfo(int type, long ssize, String timezone,
         }
     }
     
-    /**
-     * Add an image name to the database. 
-     * Intended to be called from the native code during the add image process.
-     * 
-     * @param objId    The object id of the image.
-     * @param name     The file name for the image
-     * @param sequence The sequence number of this file.
-     * 
-     * @return 0 if successful, -1 if not
-     */
-    int addImageName(long objId, String name, long sequence) {
-        try {
-            beginTransaction();
-            caseDb.addImageNameJNI(objId, name, sequence, trans);
-            commitTransaction();
-            return 0;
-        } catch (TskCoreException ex) {
-            logger.log(Level.SEVERE, "Error adding image name to the database - image obj ID: " + objId + ", image name: " + name
-                    + ", sequence: " + sequence, ex);
-            revertTransaction();
-            return -1;
-        }
-    }
-    
     /**
      * Add a volume system to the database. 
      * Intended to be called from the native code during the add image process.
@@ -348,7 +337,8 @@ private long addBatchedFilesToDb() {
         List<Long> newObjIds = new ArrayList<>();
         try {
             beginTransaction();
-            for (FileInfo fileInfo : batchedFiles) {
+            FileInfo fileInfo;
+            while ((fileInfo = batchedFiles.poll()) != null) {
                 long computedParentObjId = fileInfo.parentObjId;
                 try {
                     // If we weren't given the parent object ID, look it up
@@ -388,20 +378,20 @@ private long addBatchedFilesToDb() {
                 } catch (TskCoreException ex) {
                     logger.log(Level.SEVERE, "Error adding file to the database - parent object ID: " + computedParentObjId
                             + ", file system object ID: " + fileInfo.fsObjId + ", name: " + fileInfo.name, ex);
-                    revertTransaction();
-                    batchedFiles.clear();
-                    return -1;
                 }
             }
             commitTransaction();
-            addDataSourceCallbacks.onFilesAdded(newObjIds);
+			try {
+				addDataSourceCallbacks.onFilesAdded(newObjIds);
+			} catch (Exception ex) {
+				// Exception firewall to prevent unexpected return to the native code
+				logger.log(Level.SEVERE, "Unexpected error from files added callback", ex);
+			}
         } catch (TskCoreException ex) {
             logger.log(Level.SEVERE, "Error adding batched files to database", ex);
             revertTransaction();
-            batchedFiles.clear();
             return -1;
         }
-        batchedFiles.clear();
         return 0;
     }
     
@@ -426,10 +416,11 @@ private long getParentObjId(FileInfo fileInfo) throws TskCoreException {
         if (parentDirCache.containsKey(key)) {
             return parentDirCache.get(key);
         } else {
-            // The parent wasn't found in the cache so do a database query
-            java.io.File parentAsFile = new java.io.File(parentPath);
-            return caseDb.findParentObjIdJNI(fileInfo.parMetaAddr, fileInfo.fsObjId, parentAsFile.getPath(), parentAsFile.getName(), trans);
-        }
+            // There's no reason to do a database query since every folder added is being
+            // stored in the cache.
+            throw new TskCoreException("Parent not found in cache (fsObjId: " +fileInfo.fsObjId + ", parMetaAddr: " + fileInfo.parMetaAddr
+                + ", parSeq: " + fileInfo.parSeq + ", parentPath: " + parentPath + ")");
+		}
     }
     
     /**
@@ -474,6 +465,10 @@ long addLayoutFile(long parentObjId,
                 null, null, 
                 true, trans);
             commitTransaction();
+
+            // Store the layout file ID for later processing
+            layoutFileIds.add(objId);
+
             return objId;
         } catch (TskCoreException ex) {
             logger.log(Level.SEVERE, "Error adding layout file to the database - parent object ID: " + parentObjId
@@ -511,19 +506,16 @@ long addLayoutFileRange(long objId, long byteStart, long byteLen, long seq) {
     private long addBatchedLayoutRangesToDb() {
         try {
             beginTransaction();
-    
-            for (LayoutRangeInfo range : batchedLayoutRanges) {
+    		LayoutRangeInfo range;
+            while ((range = batchedLayoutRanges.poll()) != null) {
                 try {
                     caseDb.addLayoutFileRangeJNI(range.objId, range.byteStart, range.byteLen, range.seq, trans);
                 } catch (TskCoreException ex) {
                     logger.log(Level.SEVERE, "Error adding layout file range to the database - layout file ID: " + range.objId 
                         + ", byte start: " + range.byteStart, ex);
-                    revertTransaction();
-                    return -1;
                 }
             }
             commitTransaction();
-            batchedLayoutRanges.clear();
             return 0;
         } catch (TskCoreException ex) {
             logger.log(Level.SEVERE, "Error adding batched files to database", ex);
@@ -531,6 +523,16 @@ private long addBatchedLayoutRangesToDb() {
             return -1;
         }
     }
+	
+    /**
+     * Send completed layout files on for further processing.
+     * Note that this must wait until we know all the ranges for each
+     * file have been added to the database. 
+     */
+    void processLayoutFiles() {
+        addDataSourceCallbacks.onFilesAdded(layoutFileIds);
+        layoutFileIds.clear();
+    }
     
     /**
      * Add a virtual directory to hold unallocated file system blocks.
diff --git a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
index 4933d492aba8714ceff6cb5f27685e146b0f4f07..9d8002f52998a4b169db6a0e0c91926e458060db 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
@@ -10123,7 +10123,7 @@ public List<ContentTag> getContentTagsByContent(Content content) throws TskCoreE
 	 *         row.
 	 *
 	 * @throws TskCoreException
-	 * @Deprecated User TaggingManager.addArtifactTag instead.
+	 * @deprecated User TaggingManager.addArtifactTag instead.
 	 */
 	@Deprecated
 	public BlackboardArtifactTag addBlackboardArtifactTag(BlackboardArtifact artifact, TagName tagName, String comment) throws TskCoreException {
@@ -11074,44 +11074,6 @@ void addImageNameJNI(long objId, String name, long sequence,
 		}
 	}
 
-	/**
-	 * Looks up a parent file object ID. The calling thread is expected to have
-	 * a case read lock. For use with the JNI callbacks associated with the add
-	 * image process.
-	 *
-	 * @param metaAddr    The metadata address.
-	 * @param fsObjId     The file system object ID.
-	 * @param path        The file path.
-	 * @param name        The file name.
-	 * @param transaction The open transaction.
-	 *
-	 * @return The object ID if found, -1 otherwise.
-	 *
-	 * @throws TskCoreException
-	 */
-	long findParentObjIdJNI(long metaAddr, long fsObjId, String path, String name, CaseDbTransaction transaction) throws TskCoreException {
-		ResultSet resultSet = null;
-		try {
-			CaseDbConnection connection = transaction.getConnection();
-			PreparedStatement preparedStatement = connection.getPreparedStatement(PREPARED_STATEMENT.SELECT_OBJ_ID_BY_META_ADDR_AND_PATH);
-			preparedStatement.clearParameters();
-			preparedStatement.setLong(1, metaAddr);
-			preparedStatement.setLong(2, fsObjId);
-			preparedStatement.setString(3, path);
-			preparedStatement.setString(4, name);
-			resultSet = connection.executeQuery(preparedStatement);
-			if (resultSet.next()) {
-				return resultSet.getLong("obj_id");
-			} else {
-				throw new TskCoreException(String.format("Error looking up parent - meta addr: %d, path: %s, name: %s", metaAddr, path, name));
-			}
-		} catch (SQLException ex) {
-			throw new TskCoreException(String.format("Error looking up parent - meta addr: %d, path: %s, name: %s", metaAddr, path, name), ex);
-		} finally {
-			closeResultSet(resultSet);
-		}
-	}
-
 	/**
 	 * Add a file system file to the database. For use with the JNI callbacks
 	 * associated with the add image process.
@@ -11535,7 +11497,6 @@ private enum PREPARED_STATEMENT {
 		INSERT_POOL_INFO("INSERT INTO tsk_pool_info (obj_id, pool_type) VALUES (?, ?)"),
 		INSERT_FS_INFO("INSERT INTO tsk_fs_info (obj_id, data_source_obj_id, img_offset, fs_type, block_size, block_count, root_inum, first_inum, last_inum, display_name)"
 				+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
-		SELECT_OBJ_ID_BY_META_ADDR_AND_PATH("SELECT obj_id FROM tsk_files WHERE meta_addr = ? AND fs_obj_id = ? AND parent_path = ? AND name = ?"),
 		SELECT_TAG_NAME_BY_ID("SELECT * FROM tag_names where tag_name_id = ?");
 
 		private final String sql;
diff --git a/bindings/java/src/org/sleuthkit/datamodel/TimelineEventType.java b/bindings/java/src/org/sleuthkit/datamodel/TimelineEventType.java
index a6fabdadb4ea950bbc5d30c045ace5498e4ede3b..8f99e5af32c36792f65d390f349776e0b42f0aab 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/TimelineEventType.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/TimelineEventType.java
@@ -224,7 +224,7 @@ public int compare(TimelineEventType o1, TimelineEventType o2) {
 			builder.add(CALL_LOG, DEVICES_ATTACHED, EMAIL,
 					EXIF, GPS_BOOKMARK, GPS_LAST_KNOWN_LOCATION, GPS_TRACKPOINT,
 					GPS_ROUTE, GPS_SEARCH, GPS_TRACK, INSTALLED_PROGRAM, LOG_ENTRY, MESSAGE,
-					RECENT_DOCUMENTS, REGISTRY);
+					METADATA_LAST_PRINTED, METADATA_LAST_SAVED, METADATA_CREATED, RECENT_DOCUMENTS, REGISTRY);
 
 			return builder.build();
 		}
@@ -526,7 +526,35 @@ public SortedSet< TimelineEventType> getChildren() {
 			MISC_TYPES,
 			new BlackboardArtifact.Type(TSK_GPS_TRACK),
 			new Type(TSK_NAME));
+	
+	TimelineEventType METADATA_LAST_PRINTED = new TimelineEventArtifactTypeImpl(33,
+			getBundle().getString("MiscTypes.metadataLastPrinted.name"),// NON-NLS
+			MISC_TYPES,
+			new BlackboardArtifact.Type(TSK_METADATA),
+			new BlackboardAttribute.Type(TSK_LAST_PRINTED_DATETIME),
+            artf -> {return getBundle().getString("MiscTypes.metadataLastPrinted.name");},
+	        new EmptyExtractor(),
+	        new EmptyExtractor());
+
 
+	TimelineEventType METADATA_LAST_SAVED = new TimelineEventArtifactTypeImpl(34,
+			getBundle().getString("MiscTypes.metadataLastSaved.name"),// NON-NLS
+			MISC_TYPES,
+			new BlackboardArtifact.Type(TSK_METADATA),
+			new BlackboardAttribute.Type(TSK_DATETIME_MODIFIED),
+            artf -> {return getBundle().getString("MiscTypes.metadataLastSaved.name");},
+	        new EmptyExtractor(),
+	        new EmptyExtractor());
+
+	TimelineEventType METADATA_CREATED = new TimelineEventArtifactTypeImpl(35,
+			getBundle().getString("MiscTypes.metadataCreated.name"),// NON-NLS
+			MISC_TYPES,
+			new BlackboardArtifact.Type(TSK_METADATA),
+			new BlackboardAttribute.Type(TSK_DATETIME_CREATED),
+            artf -> {return getBundle().getString("MiscTypes.metadataCreated.name");},
+	        new EmptyExtractor(),
+	        new EmptyExtractor());
+			
 	static SortedSet<? extends TimelineEventType> getCategoryTypes() {
 		return ROOT_EVENT_TYPE.getChildren();
 	}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/GeoArtifactsHelper.java b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/GeoArtifactsHelper.java
index 558fda9b7f66d63e807050840ece29fa3bb63e84..52c433a6a04435ffc0765cc714460906b2a7ceb6 100755
--- a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/GeoArtifactsHelper.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/GeoArtifactsHelper.java
@@ -37,7 +37,7 @@
 public final class GeoArtifactsHelper extends ArtifactHelperBase {
 
 	private static final BlackboardAttribute.Type WAYPOINTS_ATTR_TYPE = new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_WAYPOINTS);
-	private static final BlackboardAttribute.Type TRACKPOINTS_ATTR_TYPE = new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_TRACKPOINTS);	
+	private static final BlackboardAttribute.Type TRACKPOINTS_ATTR_TYPE = new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_TRACKPOINTS);
 	private final String programName;
 
 	/**
@@ -67,28 +67,32 @@ public GeoArtifactsHelper(SleuthkitCase caseDb, String moduleName, String progra
 	 * (elevation) axes.
 	 *
 	 * @param trackName      The name of the GPS track, may be null.
-	 * @param trackPoints    The track points that make up the track.
+	 * @param trackPoints    The track points that make up the track. This list
+	 *                       should be non-null and non-empty.
 	 * @param moreAttributes Additional attributes for the TSK_GPS_TRACK
 	 *                       artifact, may be null.
 	 *
 	 * @return	The TSK_GPS_TRACK artifact that was added to the case database.
 	 *
-	 * @throws TskCoreException	   If there is an error creating the artifact.
-	 * @throws BlackboardException If there is a error posting the artifact to
-	 *                             the blackboard.
+	 * @throws TskCoreException	        If there is an error creating the
+	 *                                  artifact.
+	 * @throws BlackboardException      If there is a error posting the artifact
+	 *                                  to the blackboard.
+	 * @throws IllegalArgumentException If the trackpoints provided are null or
+	 *                                  empty.
 	 */
 	public BlackboardArtifact addTrack(String trackName, GeoTrackPoints trackPoints, List<BlackboardAttribute> moreAttributes) throws TskCoreException, BlackboardException {
-		if (trackPoints == null) {
-			throw new IllegalArgumentException(String.format("addTrack was passed a null list of track points"));
+		if (trackPoints == null || trackPoints.isEmpty()) {
+			throw new IllegalArgumentException(String.format("addTrack was passed a null or empty list of track points"));
 		}
 
-		BlackboardArtifact artifact = getContent().newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_TRACK);
 		List<BlackboardAttribute> attributes = new ArrayList<>();
 
 		if (trackName != null) {
 			attributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME, getModuleName(), trackName));
 		}
 
+		// acquire necessary attribute.  If 'toAttribute' call throws an exception, an artifact will not be created for this instance.
 		attributes.add(BlackboardJsonAttrUtil.toAttribute(TRACKPOINTS_ATTR_TYPE, getModuleName(), trackPoints));
 
 		if (programName != null) {
@@ -99,13 +103,14 @@ public BlackboardArtifact addTrack(String trackName, GeoTrackPoints trackPoints,
 			attributes.addAll(moreAttributes);
 		}
 
+		BlackboardArtifact artifact = getContent().newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_TRACK);
 		artifact.addAttributes(attributes);
 
 		getSleuthkitCase().getBlackboard().postArtifact(artifact, getModuleName());
 
 		return artifact;
-	}	
-	
+	}
+
 	/**
 	 * Adds a TSK_GPS_ROUTE artifact to the case database. A Global Positioning
 	 * System (GPS) route artifact records one or more waypoints entered into a
@@ -117,19 +122,23 @@ public BlackboardArtifact addTrack(String trackName, GeoTrackPoints trackPoints,
 	 * @param creationTime   The time at which the route was created as
 	 *                       milliseconds from the Java epoch of
 	 *                       1970-01-01T00:00:00Z, may be null.
-	 * @param wayPoints      The waypoints that make up the route.
+	 * @param wayPoints      The waypoints that make up the route.  This list
+	 *                       should be non-null and non-empty.
 	 * @param moreAttributes Additional attributes for the TSK_GPS_ROUTE
 	 *                       artifact, may be null.
 	 *
 	 * @return	The TSK_GPS_ROUTE artifact that was added to the case database.
 	 *
-	 * @throws TskCoreException	   If there is an error creating the artifact.
-	 * @throws BlackboardException If there is a error posting the artifact to
-	 *                             the blackboard.
+	 * @throws TskCoreException	        If there is an error creating the
+	 *                                  artifact.
+	 * @throws BlackboardException      If there is a error posting the artifact
+	 *                                  to the blackboard.
+	 * @throws IllegalArgumentException If the waypoints provided are null or
+	 *                                  empty.
 	 */
 	public BlackboardArtifact addRoute(String routeName, Long creationTime, GeoWaypoints wayPoints, List<BlackboardAttribute> moreAttributes) throws TskCoreException, BlackboardException {
-		if (wayPoints == null) {
-			throw new IllegalArgumentException(String.format("addRoute was passed a null list of waypoints"));
+		if (wayPoints == null || wayPoints.isEmpty()) {
+			throw new IllegalArgumentException(String.format("addRoute was passed a null or empty list of waypoints"));
 		}
 
 		BlackboardArtifact artifact = getContent().newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_ROUTE);
@@ -159,5 +168,5 @@ public BlackboardArtifact addRoute(String routeName, Long creationTime, GeoWaypo
 
 		return artifact;
 	}
-	
+
 }