diff --git a/NEWS.txt b/NEWS.txt
index e20c995e700e5bd2d3949d1074d87a587ae70998..a4a155747a6fec656a047581b3ac0d1baa0d1dcc 100644
--- a/NEWS.txt
+++ b/NEWS.txt
@@ -1,5 +1,31 @@
-Numbers refer to SourceForge.net tracker IDs:
-    http://sourceforge.net/tracker/?group_id=55685
+
+
+---------------- VERSION 4.7.0 --------------
+C/C++:
+- DB schema was expanded to store tsk_events and related tables.
+Time-based data is automatically added when files and artifacts are
+created.  Used by Autopsy timeline.
+- Logical Imager can save files as individual files instead of in
+VHD (saves space).
+- Logical imager produces log of results
+- Logical Imager refactor
+- Removed PRIuOFF and other macros that caused problems with
+signed/unsigned printing. For example, TSK_OFF_T is a signed value
+and PRIuOFF would cause problems as it printed a negative number
+as a big positive number.
+
+
+Java
+- Travis and Debian package use OpenJDK instead of OracleJDK
+- New Blackboard Helper packages (blackboardutils) to make it easier
+to make artifacts.
+- Blackboard scope was expanded, including the new postArtifact() method
+that adds event data to database and broadcasts an event to listeners.
+- SleuthkitCase now has an EventBus for database-related events.
+- New TimelineManager and associated filter classes to support new events 
+table
+
+
 
 ---------------- VERSION 4.6.7 --------------
 C/C++ Code:
diff --git a/bindings/java/build.xml b/bindings/java/build.xml
index 52615de6c7410bce09f210ea57162433e5bcbba3..124bc42d75acfa314c56531f28fce1f244c94266 100644
--- a/bindings/java/build.xml
+++ b/bindings/java/build.xml
@@ -11,7 +11,7 @@
 	<import file="build-${os.family}.xml"/>
 
     <!-- Careful changing this because release-windows.pl updates it by pattern -->
-<property name="VERSION" value="4.6.7"/>
+<property name="VERSION" value="4.7.0"/>
 
 	<!-- set global properties for this build -->
 	<property name="default-jar-location" location="/usr/share/java"/>
diff --git a/bindings/java/src/org/sleuthkit/datamodel/Account.java b/bindings/java/src/org/sleuthkit/datamodel/Account.java
index 0d7cb68006ce112fd79f59192848c8c08b8f68dd..bec66e453181c5f31af7afebf726adfb0ddb81c9 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/Account.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/Account.java
@@ -41,42 +41,6 @@ public final class Account {
 	 */
 	private final String typeSpecificID;
 
-	/**
-	 * Class to abstract an account address.
-	 * An account address comprises of a unique id and a display name.
-	 */
-	public static final class Address {
-		
-		// account type specific unique id
-		private final String uniqueID;
-		
-		// Display name for account
-		private final String displayName;
-		
-		public Address(String uniqueID, String displayName  ) {
-			this.uniqueID = uniqueID;
-			this.displayName = displayName;
-		}
-		
-		/**
-		 * Account type specific unique ID 
-		 *
-		 * @return The type name.
-		 */
-		public String getUniqueID() {
-			return this.uniqueID;
-		}
-
-		/**
-		 * Gets the display name
-		 *
-		 * @return The display name.
-		 */
-		public String getDisplayName() {
-			return displayName;
-		}	
-	}
-	
 	public static final class Type {
 
 		//JIRA-900:Should the display names of predefined types be internationalized?
diff --git a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties
index 2ff00592feba01f9e3a2819d9a5f0002e1addae0..8e865dbff2558e52031ff5d872e6d9715df76850 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties
+++ b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties
@@ -285,10 +285,9 @@ IngestModuleInfo.IngestModuleType.DataSourceLevel.displayName=Data Source Level
 ReviewStatus.Approved=Approved
 ReviewStatus.Rejected=Rejected
 ReviewStatus.Undecided=Undecided
-DescriptionLOD.short=Short
-DescriptionLOD.medium=Medium
-DescriptionLOD.full=Full
-
+TimelineLevelOfDetail.low=Low
+TimelineLevelOfDetail.medium=Medium
+TimelineLevelOfDetail.high=High
 BaseTypes.fileSystem.name=File System
 BaseTypes.webActivity.name=Web Activity
 BaseTypes.miscTypes.name=Misc Types
@@ -318,12 +317,9 @@ WebTypes.webFormAddress.name=Web Form Address
 CustomTypes.other.name=Other
 CustomTypes.userCreated.name=User Created
 BaseTypes.customTypes.name=Custom Types
-
-
-EventTypeZoomLevel.rootType=Root Type
-EventTypeZoomLevel.baseType=Base Type
-EventTypeZoomLevel.subType=Sub Type
-
+EventTypeHierarchyLevel.root=Root
+EventTypeHierarchyLevel.category=Category
+EventTypeHierarchyLevel.event=Event
 DataSourcesFilter.displayName.text=Data Source
 DescriptionFilter.mode.exclude=Exclude
 DescriptionFilter.mode.include=Include
diff --git a/bindings/java/src/org/sleuthkit/datamodel/TimelineEvent.java b/bindings/java/src/org/sleuthkit/datamodel/TimelineEvent.java
index b1082960494b387ce79988b5902906e275b38278..23e5f376e124b270d039e157eafd60026e4a9bbf 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/TimelineEvent.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/TimelineEvent.java
@@ -19,34 +19,39 @@
 package org.sleuthkit.datamodel;
 
 import java.util.Optional;
-import java.util.ResourceBundle;
-import static org.sleuthkit.datamodel.TimelineEventType.TypeLevel.SUB_TYPE;
 
 /**
- * A single event.
+ * A representation of an event in the timeline of a case.
  */
 public final class TimelineEvent {
 
+	/**
+	 * The unique ID of this event in the case database.
+	 */
 	private final long eventID;
 
 	/**
-	 * The TSK object ID of the file this event is derived from.
+	 * The object ID of the content that is either the direct or indirect source
+	 * of this event. For events associated with files, this will be the object
+	 * ID of the file. For events associated with artifacts, this will be the
+	 * object ID of the artifact source: a file, a data source, or another
+	 * artifact.
 	 */
-	private final long fileObjID;
+	private final long contentObjID;
 
 	/**
-	 * The TSK artifact ID of the file this event is derived from. Null, if this
-	 * event is not derived from an artifact.
+	 * The artifact ID (not the object ID) of the artifact, if any, that is the
+	 * source of this event. Null for events assoicated directly with files.
 	 */
 	private final Long artifactID;
 
 	/**
-	 * The TSK datasource ID of the datasource this event belongs to.
+	 * The object ID of the data source for the event source.
 	 */
 	private final long dataSourceObjID;
 
 	/**
-	 * The time of this event in second from the Unix epoch.
+	 * When this event occurred, in seconds from the UNIX epoch.
 	 */
 	private final long time;
 
@@ -56,179 +61,191 @@ public final class TimelineEvent {
 	private final TimelineEventType type;
 
 	/**
-	 * The three descriptions (full, med, short) stored in a map, keyed by
-	 * DescriptionLOD (TypeLevel of Detail)
+	 * The description of this event, provided at three levels of detail: high
+	 * (full description), medium (medium description), and low (short
+	 * description).
 	 */
 	private final TimelineEventDescription descriptions;
 
 	/**
-	 * True if the file this event is derived from hits any of the configured
-	 * hash sets.
+	 * True if the file, if any, associated with this event, either directly or
+	 * indirectly, is a file for which a hash set hit has been detected.
 	 */
-	private final boolean hashHit;
+	private final boolean eventSourceHashHitDetected;
 
 	/**
-	 * True if the file or artifact this event is derived from is tagged.
+	 * True if the direct source (file or artifact) of this event has been
+	 * tagged.
 	 */
-	private final boolean tagged;
+	private final boolean eventSourceTagged;
 
 	/**
+	 * Constructs a representation of an event in the timeline of a case.
 	 *
-	 * @param eventID         ID from tsk_events table in database
-	 * @param dataSourceObjID Object Id for data source event is from
-	 * @param fileObjID       object id for non-artifact content that event is
-	 *                        associated with
-	 * @param artifactID      ID of artifact (not object id) if event came from
-	 *                        an artifact
-	 * @param time
-	 * @param type
-	 * @param descriptions
-	 * @param hashHit
-	 * @param tagged
+	 * @param eventID                    The unique ID of this event in the case
+	 *                                   database.
+	 * @param dataSourceObjID            The object ID of the data source for
+	 *                                   the event source.
+	 * @param contentObjID               The object ID of the content that is
+	 *                                   either the direct or indirect source of
+	 *                                   this event. For events associated with
+	 *                                   files, this will be the object ID of
+	 *                                   the file. For events associated with
+	 *                                   artifacts, this will be the object ID
+	 *                                   of the artifact source: a file, a data
+	 *                                   source, or another artifact.
+	 * @param artifactID                 The artifact ID (not the object ID) of
+	 *                                   the artifact, if any, that is the
+	 *                                   source of this event. Null for events
+	 *                                   assoicated directly with files.
+	 * @param time                       The time this event occurred, in
+	 *                                   seconds from the UNIX epoch.
+	 * @param type                       The type of this event.
+	 * @param fullDescription            The full length description of this
+	 *                                   event.
+	 * @param medDescription             The medium length description of this
+	 *                                   event.
+	 * @param shortDescription           The short length description of this
+	 *                                   event.
+	 * @param eventSourceHashHitDetected True if the file, if any, associated
+	 *                                   with this event, either directly or
+	 *                                   indirectly, is a file for which a hash
+	 *                                   set hit has been detected.
+	 * @param eventSourceTagged          True if the direct source (file or
+	 *                                   artifact) of this event has been
+	 *                                   tagged.
 	 */
-	TimelineEvent(long eventID, long dataSourceObjID, long fileObjID, Long artifactID,
-			long time, TimelineEventType type,
+	TimelineEvent(long eventID,
+			long dataSourceObjID,
+			long contentObjID,
+			Long artifactID,
+			long time,
+			TimelineEventType type,
 			String fullDescription,
 			String medDescription,
 			String shortDescription,
-			boolean hashHit, boolean tagged) {
+			boolean eventSourceHashHitDetected,
+			boolean eventSourceTagged) {
 		this.eventID = eventID;
 		this.dataSourceObjID = dataSourceObjID;
-		this.fileObjID = fileObjID;
+		this.contentObjID = contentObjID;
 		this.artifactID = Long.valueOf(0).equals(artifactID) ? null : artifactID;
 		this.time = time;
 		this.type = type;
-		// This isn't the best design, but it was the most expediant way to reduce 
-		// the public API (by keeping parseDescription()) out of the public API.  
+		/*
+		 * The cast that follows reflects the fact that we have not decided
+		 * whether or not to add the parseDescription method to the
+		 * TimelineEventType interface yet. Currently (9/18/19), this method is
+		 * part of TimelineEventTypeImpl and all implementations of
+		 * TimelineEventType are subclasses of TimelineEventTypeImpl.
+		 */
 		if (type instanceof TimelineEventTypeImpl) {
 			this.descriptions = ((TimelineEventTypeImpl) type).parseDescription(fullDescription, medDescription, shortDescription);
 		} else {
-			throw new IllegalArgumentException();
+			this.descriptions = new TimelineEventDescription(fullDescription, medDescription, shortDescription);
 		}
-		this.hashHit = hashHit;
-		this.tagged = tagged;
+		this.eventSourceHashHitDetected = eventSourceHashHitDetected;
+		this.eventSourceTagged = eventSourceTagged;
 	}
 
 	/**
-	 * Is the file or artifact this event is derived from tagged?
+	 * Indicates whether or not the direct source (file or artifact) of this
+	 * artifact has been tagged.
 	 *
-	 * @return true if he file or artifact this event is derived from is tagged.
+	 * @return True or false.
 	 */
-	public boolean isTagged() {
-		return tagged;
+	public boolean eventSourceIsTagged() {
+		return eventSourceTagged;
 	}
 
 	/**
-	 * Is the file this event is derived from in any of the configured hash
-	 * sets.
-	 *
+	 * Indicates whether or not the file, if any, associated with this event,
+	 * either directly or indirectly, is a file for which a hash set hit has
+	 * been detected.
 	 *
-	 * @return True if the file this event is derived from is in any of the
-	 *         configured hash sets.
+	 * @return True or false.
 	 */
-	public boolean isHashHit() {
-		return hashHit;
+	public boolean eventSourceHasHashHits() {
+		return eventSourceHashHitDetected;
 	}
 
 	/**
-	 * Get the artifact id (not the object ID) of the artifact this event is
-	 * derived from.
+	 * Gets the artifact ID (not object ID) of the artifact, if any, that is the
+	 * direct source of this event.
 	 *
-	 * @return An Optional containing the artifact ID. Will be empty if this
-	 *         event is not derived from an artifact
+	 * @return An Optional object containing the artifact ID. May be empty.
 	 */
 	public Optional<Long> getArtifactID() {
 		return Optional.ofNullable(artifactID);
 	}
 
 	/**
-	 * Get the event id of this event.
+	 * Gets the unique ID of this event in the case database.
 	 *
-	 * @return The event id of this event.
+	 * @return The event ID.
 	 */
 	public long getEventID() {
 		return eventID;
 	}
 
 	/**
-	 * Get the Content obj id of the "file" (which could be a data source or
-	 * other non AbstractFile ContentS) this event is derived from.
+	 * Gets the object ID of the content that is the direct or indirect source
+	 * of this event. For events associated with files, this will be the object
+	 * ID of the file that is the direct event source. For events associated
+	 * with artifacts, this will be the object ID of the artifact source: a
+	 * file, a data source, or another artifact.
 	 *
-	 * @return the object id.
+	 * @return The object ID.
 	 */
-	public long getFileObjID() {
-		return fileObjID;
+	public long getContentObjID() {
+		return contentObjID;
 	}
 
 	/**
-	 * Get the time of this event (in seconds from the Unix epoch).
+	 * Gets the time this event occurred.
 	 *
-	 * @return the time of this event in seconds from Unix epoch
+	 * @return The time this event occurred, in seconds from UNIX epoch.
 	 */
 	public long getTime() {
 		return time;
 	}
 
-	public TimelineEventType getEventType() {
-		return type;
-	}
-
-	public TimelineEventType getEventType(TimelineEventType.TypeLevel zoomLevel) {
-		return zoomLevel.equals(SUB_TYPE) ? type : type.getBaseType();
-	}
-
-	/**
-	 * Get the full description of this event.
-	 *
-	 * @return the full description
-	 */
-	public String getFullDescription() {
-		return getDescription(TimelineEvent.DescriptionLevel.FULL);
-	}
-
-	/**
-	 * Get the medium description of this event.
-	 *
-	 * @return the medium description
-	 */
-	public String getMedDescription() {
-		return getDescription(TimelineEvent.DescriptionLevel.MEDIUM);
-	}
-
 	/**
-	 * Get the short description of this event.
+	 * Gets the type of this event.
 	 *
-	 * @return the short description
+	 * @return The event type.
 	 */
-	public String getShortDescription() {
-		return getDescription(TimelineEvent.DescriptionLevel.SHORT);
+	public TimelineEventType getEventType() {
+		return type;
 	}
 
 	/**
-	 * Get the description of this event at the give level of detail(LoD).
+	 * Gets the description of this event at a given level of detail.
 	 *
-	 * @param lod The level of detail to get.
+	 * @param levelOfDetail The desired level of detail.
 	 *
 	 * @return The description of this event at the given level of detail.
 	 */
-	public String getDescription(TimelineEvent.DescriptionLevel lod) {
-		return descriptions.getDescription(lod);
+	public String getDescription(TimelineLevelOfDetail levelOfDetail) {
+		return descriptions.getDescription(levelOfDetail);
 	}
 
 	/**
-	 * Get the datasource id of the datasource this event belongs to.
+	 * Gets the object ID of the data source for the source content of this
+	 * event.
 	 *
-	 * @return the datasource id.
+	 * @return The data source object ID.
 	 */
 	public long getDataSourceObjID() {
 		return dataSourceObjID;
 	}
 
-	public long getEndMillis() {
-		return time * 1000;
-	}
-
-	public long getStartMillis() {
+	/**
+	 * Gets the time this event occured, in milliseconds from the UNIX epoch.
+	 *
+	 * @return The event time in milliseconds from the UNIX epoch.
+	 */
+	public long getEventTimeInMs() {
 		return time * 1000;
 	}
 
@@ -248,42 +265,7 @@ public boolean equals(Object obj) {
 			return false;
 		}
 		final TimelineEvent other = (TimelineEvent) obj;
-		return this.eventID == other.eventID;
+		return this.eventID == other.getEventID();
 	}
 
-	/**
-	 * Defines the zoom levels that are available for the event description
-	 */
-	public enum DescriptionLevel {
-		SHORT(ResourceBundle.getBundle("org.sleuthkit.datamodel.Bundle").getString("DescriptionLOD.short")),
-		MEDIUM(ResourceBundle.getBundle("org.sleuthkit.datamodel.Bundle").getString("DescriptionLOD.medium")),
-		FULL(ResourceBundle.getBundle("org.sleuthkit.datamodel.Bundle").getString("DescriptionLOD.full"));
-
-		private final String displayName;
-
-		public String getDisplayName() {
-			return displayName;
-		}
-
-		private DescriptionLevel(String displayName) {
-			this.displayName = displayName;
-		}
-
-		public DescriptionLevel moreDetailed() {
-			try {
-				return values()[ordinal() + 1];
-			} catch (ArrayIndexOutOfBoundsException e) {
-				return null;
-			}
-		}
-
-		public DescriptionLevel lessDetailed() {
-			try {
-				return values()[ordinal() - 1];
-			} catch (ArrayIndexOutOfBoundsException e) {
-				return null;
-			}
-		}
-
-	}
 }
diff --git a/bindings/java/src/org/sleuthkit/datamodel/TimelineEventArtifactTypeImpl.java b/bindings/java/src/org/sleuthkit/datamodel/TimelineEventArtifactTypeImpl.java
index bee1ca8845cca7ee63e1849c06ba9c1439f2ac1a..0f35b1c2cbf17e49faab8eddfd68a9b03c375d2c 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/TimelineEventArtifactTypeImpl.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/TimelineEventArtifactTypeImpl.java
@@ -29,16 +29,23 @@
 /**
  * Version of TimelineEventType for events based on artifacts
  */
-class TimelineEventArtifactTypeImpl extends TimelineEventTypeImpl { 
+class TimelineEventArtifactTypeImpl extends TimelineEventTypeImpl {
 
 	private static final Logger logger = Logger.getLogger(TimelineEventArtifactTypeImpl.class.getName());
-
+	
+	static final int EMAIL_FULL_DESCRIPTION_LENGTH_MAX = 150;
+	static final int EMAIL_TO_FROM_LENGTH_MAX = 75;
+	
 	private final BlackboardArtifact.Type artifactType;
 	private final BlackboardAttribute.Type dateTimeAttributeType;
 	private final TSKCoreCheckedFunction<BlackboardArtifact, String> fullExtractor;
 	private final TSKCoreCheckedFunction<BlackboardArtifact, String> medExtractor;
 	private final TSKCoreCheckedFunction<BlackboardArtifact, String> shortExtractor;
 	private final TSKCoreCheckedFunction<BlackboardArtifact, TimelineEventDescriptionWithTime> artifactParsingFunction;
+	
+	private static final int MAX_SHORT_DESCRIPTION_LENGTH = 500;
+	private static final int MAX_MED_DESCRIPTION_LENGTH = 500;
+	private static final int MAX_FULL_DESCRIPTION_LENGTH = 1024;
 
 	TimelineEventArtifactTypeImpl(int typeID, String displayName,
 			TimelineEventType superType,
@@ -59,7 +66,7 @@ class TimelineEventArtifactTypeImpl extends TimelineEventTypeImpl {
 			TSKCoreCheckedFunction<BlackboardArtifact, String> fullExtractor,
 			TSKCoreCheckedFunction<BlackboardArtifact, TimelineEventDescriptionWithTime> eventPayloadFunction) {
 
-		super(typeID, displayName, TimelineEventType.TypeLevel.SUB_TYPE, superType);
+		super(typeID, displayName, TimelineEventType.HierarchyLevel.EVENT, superType);
 		this.artifactType = artifactType;
 		this.dateTimeAttributeType = dateTimeAttributeType;
 		this.shortExtractor = shortExtractor;
@@ -102,13 +109,14 @@ BlackboardArtifact.Type getArtifactType() {
 		return artifactType;
 	}
 
-	
 	/**
 	 * Parses the artifact to create a triple description with a time.
-	 * 
+	 *
 	 * @param artifact
+	 *
 	 * @return
-	 * @throws TskCoreException 
+	 *
+	 * @throws TskCoreException
 	 */
 	TimelineEventDescriptionWithTime makeEventDescription(BlackboardArtifact artifact) throws TskCoreException {
 		//if we got passed an artifact that doesn't correspond to this event type, 
@@ -122,7 +130,9 @@ TimelineEventDescriptionWithTime makeEventDescription(BlackboardArtifact artifac
 			return null;
 		}
 
-		/* Use the type-specific method */
+		/*
+		 * Use the type-specific method
+		 */
 		if (this.artifactParsingFunction != null) {
 			//use the hook provided by this subtype implementation to build the descriptions.
 			return this.artifactParsingFunction.apply(artifact);
@@ -130,8 +140,20 @@ TimelineEventDescriptionWithTime makeEventDescription(BlackboardArtifact artifac
 
 		//combine descriptions in standard way
 		String shortDescription = extractShortDescription(artifact);
+		if (shortDescription.length() > MAX_SHORT_DESCRIPTION_LENGTH) {
+			shortDescription = shortDescription.substring(0, MAX_SHORT_DESCRIPTION_LENGTH);
+		}
+
 		String medDescription = shortDescription + " : " + extractMedDescription(artifact);
+		if (medDescription.length() > MAX_MED_DESCRIPTION_LENGTH) {
+			medDescription = medDescription.substring(0, MAX_MED_DESCRIPTION_LENGTH);
+		}
+
 		String fullDescription = medDescription + " : " + extractFullDescription(artifact);
+		if (fullDescription.length() > MAX_FULL_DESCRIPTION_LENGTH) {
+			fullDescription = fullDescription.substring(0, MAX_FULL_DESCRIPTION_LENGTH);
+		}
+		
 		return new TimelineEventDescriptionWithTime(timeAttribute.getValueLong(), shortDescription, medDescription, fullDescription);
 	}
 
@@ -204,6 +226,7 @@ public String apply(BlackboardArtifact artf) throws TskCoreException {
 	 */
 	@FunctionalInterface
 	interface TSKCoreCheckedFunction<I, O> {
+
 		O apply(I input) throws TskCoreException;
 	}
 }
diff --git a/bindings/java/src/org/sleuthkit/datamodel/TimelineEventDescription.java b/bindings/java/src/org/sleuthkit/datamodel/TimelineEventDescription.java
index 1a58ee2c9ffa61720004b47d3f523a0b4181553d..0adf594c74edc009f4e0a5e10b67811250eb3c2c 100755
--- a/bindings/java/src/org/sleuthkit/datamodel/TimelineEventDescription.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/TimelineEventDescription.java
@@ -19,21 +19,40 @@
 package org.sleuthkit.datamodel;
 
 /**
- * Encapsulates the potential multiple levels of description for an event in to
- * one object. Currently used for interim storage.
+ * A container for a timeline event description with potentially varying levels
+ * of detail.
  */
 class TimelineEventDescription {
 
-	String shortDesc;
-	String mediumDesc;
-	String fullDesc;
+	private final String shortDesc;
+	private final String mediumDesc;
+	private final String fullDesc;
 
+	/**
+	 * Constructs a container for a timeline event description that varies with
+	 * each of three levels of detail.
+	 *
+	 * @param fullDescription  The full length description of an event for use
+	 *                         at a high level of detail.
+	 * @param medDescription   The medium length description of an event for use
+	 *                         at a medium level of detail.
+	 * @param shortDescription The short length description of an event for use
+	 *                         at a low level of detail.
+	 */
 	TimelineEventDescription(String fullDescription, String medDescription, String shortDescription) {
 		this.shortDesc = shortDescription;
 		this.mediumDesc = medDescription;
 		this.fullDesc = fullDescription;
 	}
 
+	/**
+	 * Constructs a container for a timeline event description for the high
+	 * level of detail. The descriptions for the low and medium levels of detail
+	 * will be the empty string.
+	 *
+	 * @param fullDescription The full length description of an event for use at
+	 *                        a high level of detail.
+	 */
 	TimelineEventDescription(String fullDescription) {
 		this.shortDesc = "";
 		this.mediumDesc = "";
@@ -41,48 +60,22 @@ class TimelineEventDescription {
 	}
 
 	/**
-	 * Get the full description of this event.
-	 *
-	 * @return the full description
-	 */
-	String getFullDescription() {
-		return fullDesc;
-	}
-
-	/**
-	 * Get the medium description of this event.
-	 *
-	 * @return the medium description
-	 */
-	String getMediumDescription() {
-		return mediumDesc;
-	}
-
-	/**
-	 * Get the short description of this event.
-	 *
-	 * @return the short description
-	 */
-	String getShortDescription() {
-		return shortDesc;
-	}
-
-	/**
-	 * Get the description of this event at the give level of detail(LoD).
+	 * Gets the description of this event at the given level of detail.
 	 *
-	 * @param lod The level of detail to get.
+	 * @param levelOfDetail The level of detail.
 	 *
-	 * @return The description of this event at the given level of detail.
+	 * @return The event description at the given level of detail.
 	 */
-	String getDescription(TimelineEvent.DescriptionLevel lod) {
-		switch (lod) {
-			case FULL:
+	String getDescription(TimelineLevelOfDetail levelOfDetail) {
+		switch (levelOfDetail) {
+			case HIGH:
 			default:
-				return getFullDescription();
+				return this.fullDesc;
 			case MEDIUM:
-				return getMediumDescription();
-			case SHORT:
-				return getShortDescription();
+				return this.mediumDesc;
+			case LOW:
+				return this.shortDesc;
 		}
 	}
+	
 }
diff --git a/bindings/java/src/org/sleuthkit/datamodel/TimelineEventType.java b/bindings/java/src/org/sleuthkit/datamodel/TimelineEventType.java
index 4d1563b7957a805defa02c5824541ce57353a9d9..3eff0b5afdbcf78583ca1120921a063d501a4da2 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/TimelineEventType.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/TimelineEventType.java
@@ -18,6 +18,7 @@
  */
 package org.sleuthkit.datamodel;
 
+import com.google.common.annotations.Beta;
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableSortedSet;
 import java.util.Arrays;
@@ -37,95 +38,140 @@
 import static org.sleuthkit.datamodel.TimelineEventArtifactTypeImpl.getAttributeSafe;
 
 /**
- * Interface for distinct kinds of events (ie file system or web
- * activity) in a hierarchy. An TimelineEventType may have an optional 
- super-type and 0 or more subtypes.   NOTE: this is not currently
- extensible by modules. The structure is hard coded to a certain
- number of levels and types. 
+ * An interface implemented by timeline event types. Timeline event types are
+ * organized into a type hierarchy. This type hierarchy has three levels: the
+ * root level, the category level (e.g, file system events, web activity
+ * events), and the actual event level (e.g., file modified events, web download
+ * events).
+ *
+ * Currently (9/20/19), all supported timeline event types are defined as
+ * members of this interface.
+ *
+ * WARNING: THIS INTERFACE IS A "BETA" INTERFACE AND IS SUBJECT TO CHANGE AT ANY
+ * TIME.
  */
+@Beta
 public interface TimelineEventType extends Comparable<TimelineEventType> {
-	
-	static final int EMAIL_FULL_DESCRIPTION_LENGTH_MAX = 150;
 
+	/**
+	 * Gets the display name of this event type.
+	 *
+	 * @return The event type display name.
+	 */
 	String getDisplayName();
 
 	/**
-	 * 
-	 * @return Unique type iD (from database)
+	 * Gets the unique ID of this event type in the case database.
+	 *
+	 * @return The event type ID.
 	 */
 	long getTypeID();
 
 	/**
-	 * 
-	 * @return The level that this event is in the type hierarchy.
+	 * Gets the type hierarchy level of this event type.
+	 *
+	 * @return The type hierarchy level.
 	 */
-	TimelineEventType.TypeLevel getTypeLevel();
+	TimelineEventType.HierarchyLevel getTypeHierarchyLevel();
 
 	/**
-	 * @return A list of TimelineEventTypes, one for each subtype of this EventTYpe, or
-         an empty set if this TimelineEventType has no subtypes.
+	 * Gets the child event types of this event type in the type hierarchy.
+	 *
+	 * @return A sorted set of the child event types.
 	 */
-	SortedSet<? extends TimelineEventType> getSubTypes();
-
-	Optional<? extends TimelineEventType> getSubType(String string);
-
+	SortedSet<? extends TimelineEventType> getChildren();
 
 	/**
-	 * @return the super type of this event
+	 * Gets a specific child event type of this event type in the type
+	 * hierarchy.
+	 *
+	 * @param displayName The display name of the desired child event type.
+	 *
+	 * @return The child event type in an Optional object, may be empty.
 	 */
-	TimelineEventType getSuperType();
+	Optional<? extends TimelineEventType> getChild(String displayName);
 
-	default TimelineEventType getBaseType() {
-		TimelineEventType superType = getSuperType();
+	/**
+	 * Gets the parent event type of this event type in the type hierarchy.
+	 *
+	 * @return The parent event type.
+	 */
+	TimelineEventType getParent();
 
-		return superType.equals(ROOT_EVENT_TYPE)
+	/**
+	 * Gets the category level event type for this event type in the type
+	 * hierarchy.
+	 *
+	 * @return The category event type.
+	 */
+	default TimelineEventType getCategory() {
+		TimelineEventType parentType = getParent();
+		return parentType.equals(ROOT_EVENT_TYPE)
 				? this
-				: superType.getBaseType();
-
+				: parentType.getCategory();
 	}
 
-	default SortedSet<? extends TimelineEventType> getSiblingTypes() {
+	/**
+	 * Gets the sibling event types of this event type in the type hierarchy.
+	 *
+	 * @return The sibling event types.
+	 */
+	default SortedSet<? extends TimelineEventType> getSiblings() {
 		return this.equals(ROOT_EVENT_TYPE)
 				? ImmutableSortedSet.of(ROOT_EVENT_TYPE)
-				: this.getSuperType().getSubTypes();
-
+				: this.getParent().getChildren();
 	}
 
 	@Override
 	default int compareTo(TimelineEventType otherType) {
 		return Comparator.comparing(TimelineEventType::getTypeID).compare(this, otherType);
 	}
-	
+
 	/**
-	 * Enum of event type zoom levels.
+	 * An enumeration of the levels in the event type hierarchy.
 	 */
-	public enum TypeLevel {
+	public enum HierarchyLevel {
+
 		/**
-		 * The root event type zoom level. All event are the same type at this
-		 * level.
+		 * The root level of the event types hierarchy.
 		 */
-		ROOT_TYPE(getBundle().getString("EventTypeZoomLevel.rootType")),
+		ROOT(getBundle().getString("EventTypeHierarchyLevel.root")),
 		/**
-		 * The zoom level of base event types like files system, and web activity
+		 * The category level of the event types hierarchy. Event types at this
+		 * level represent event categories such as file system events and web
+		 * activity events.
 		 */
-		BASE_TYPE(getBundle().getString("EventTypeZoomLevel.baseType")),
+		CATEGORY(getBundle().getString("EventTypeHierarchyLevel.category")),
 		/**
-		 * The zoom level of specific type such as file modified time, or web
-		 * download.
+		 * The actual events level of the event types hierarchy. Event types at
+		 * this level represent actual events such as file modified time events
+		 * and web download events.
 		 */
-		SUB_TYPE(getBundle().getString("EventTypeZoomLevel.subType"));
+		EVENT(getBundle().getString("EventTypeHierarchyLevel.event"));
 
 		private final String displayName;
 
+		/**
+		 * Gets the display name of this element of the enumeration of the
+		 * levels in the event type hierarchy.
+		 *
+		 * @return The display name.
+		 */
 		public String getDisplayName() {
 			return displayName;
 		}
 
-		private TypeLevel(String displayName) {
+		/**
+		 * Constructs an element of the enumeration of the levels in the event
+		 * type hierarchy.
+		 *
+		 * @param displayName The display name of this hierarchy level.
+		 */
+		private HierarchyLevel(String displayName) {
 			this.displayName = displayName;
 		}
-	}
 
+	}
 
 	/**
 	 * The root type of all event types. No event should actually have this
@@ -133,36 +179,38 @@ private TypeLevel(String displayName) {
 	 */
 	TimelineEventType ROOT_EVENT_TYPE = new TimelineEventTypeImpl(0,
 			getBundle().getString("RootEventType.eventTypes.name"), // NON-NLS
-			TypeLevel.ROOT_TYPE, null) {
+			HierarchyLevel.ROOT, null) {
 		@Override
-		public SortedSet< TimelineEventType> getSubTypes() {
+		public SortedSet< TimelineEventType> getChildren() {
 			return ImmutableSortedSet.of(FILE_SYSTEM, WEB_ACTIVITY, MISC_TYPES, CUSTOM_TYPES);
 		}
 	};
 
 	TimelineEventType FILE_SYSTEM = new TimelineEventTypeImpl(1,
 			getBundle().getString("BaseTypes.fileSystem.name"),// NON-NLS
-			TypeLevel.BASE_TYPE, ROOT_EVENT_TYPE) {
+			HierarchyLevel.CATEGORY, ROOT_EVENT_TYPE) {
 		@Override
-		public SortedSet< TimelineEventType> getSubTypes() {
+		public SortedSet< TimelineEventType> getChildren() {
 			return ImmutableSortedSet.of(FILE_MODIFIED, FILE_ACCESSED,
 					FILE_CREATED, FILE_CHANGED);
 		}
 	};
+
 	TimelineEventType WEB_ACTIVITY = new TimelineEventTypeImpl(2,
 			getBundle().getString("BaseTypes.webActivity.name"), // NON-NLS
-			TypeLevel.BASE_TYPE, ROOT_EVENT_TYPE) {
+			HierarchyLevel.CATEGORY, ROOT_EVENT_TYPE) {
 		@Override
-		public SortedSet< TimelineEventType> getSubTypes() {
+		public SortedSet< TimelineEventType> getChildren() {
 			return ImmutableSortedSet.of(WEB_DOWNLOADS, WEB_COOKIE, WEB_BOOKMARK,
 					WEB_HISTORY, WEB_SEARCH, WEB_FORM_AUTOFILL, WEB_FORM_ADDRESSES);
 		}
 	};
+
 	TimelineEventType MISC_TYPES = new TimelineEventTypeImpl(3,
 			getBundle().getString("BaseTypes.miscTypes.name"), // NON-NLS
-			TypeLevel.BASE_TYPE, ROOT_EVENT_TYPE) {
+			HierarchyLevel.CATEGORY, ROOT_EVENT_TYPE) {
 		@Override
-		public SortedSet<TimelineEventType> getSubTypes() {
+		public SortedSet<TimelineEventType> getChildren() {
 			return ImmutableSortedSet.of(CALL_LOG, DEVICES_ATTACHED, EMAIL,
 					EXIF, GPS_ROUTE, GPS_TRACKPOINT, INSTALLED_PROGRAM, MESSAGE,
 					RECENT_DOCUMENTS, REGISTRY, LOG_ENTRY);
@@ -171,16 +219,19 @@ public SortedSet<TimelineEventType> getSubTypes() {
 
 	TimelineEventType FILE_MODIFIED = new FilePathEventType(4,
 			getBundle().getString("FileSystemTypes.fileModified.name"), // NON-NLS
-			TypeLevel.SUB_TYPE, FILE_SYSTEM);
+			HierarchyLevel.EVENT, FILE_SYSTEM);
+	
 	TimelineEventType FILE_ACCESSED = new FilePathEventType(5,
 			getBundle().getString("FileSystemTypes.fileAccessed.name"), // NON-NLS
-			TypeLevel.SUB_TYPE, FILE_SYSTEM);
+			HierarchyLevel.EVENT, FILE_SYSTEM);
+	
 	TimelineEventType FILE_CREATED = new FilePathEventType(6,
 			getBundle().getString("FileSystemTypes.fileCreated.name"), // NON-NLS
-			TypeLevel.SUB_TYPE, FILE_SYSTEM);
+			HierarchyLevel.EVENT, FILE_SYSTEM);
+	
 	TimelineEventType FILE_CHANGED = new FilePathEventType(7,
 			getBundle().getString("FileSystemTypes.fileChanged.name"), // NON-NLS
-			TypeLevel.SUB_TYPE, FILE_SYSTEM);
+			HierarchyLevel.EVENT, FILE_SYSTEM);
 
 	TimelineEventType WEB_DOWNLOADS = new URLArtifactEventType(8,
 			getBundle().getString("WebTypes.webDownloads.name"), // NON-NLS
@@ -188,24 +239,28 @@ public SortedSet<TimelineEventType> getSubTypes() {
 			new BlackboardArtifact.Type(TSK_WEB_DOWNLOAD),
 			new Type(TSK_DATETIME_ACCESSED),
 			new Type(TSK_URL));
+	
 	TimelineEventType WEB_COOKIE = new URLArtifactEventType(9,
 			getBundle().getString("WebTypes.webCookies.name"),// NON-NLS
 			WEB_ACTIVITY,
 			new BlackboardArtifact.Type(TSK_WEB_COOKIE),
 			new Type(TSK_DATETIME),
 			new Type(TSK_URL));
+	
 	TimelineEventType WEB_BOOKMARK = new URLArtifactEventType(10,
 			getBundle().getString("WebTypes.webBookmarks.name"), // NON-NLS
 			WEB_ACTIVITY,
 			new BlackboardArtifact.Type(TSK_WEB_BOOKMARK),
 			new Type(TSK_DATETIME_CREATED),
 			new Type(TSK_URL));
+	
 	TimelineEventType WEB_HISTORY = new URLArtifactEventType(11,
 			getBundle().getString("WebTypes.webHistory.name"), // NON-NLS
 			WEB_ACTIVITY,
 			new BlackboardArtifact.Type(TSK_WEB_HISTORY),
 			new Type(TSK_DATETIME_ACCESSED),
 			new Type(TSK_URL));
+	
 	TimelineEventType WEB_SEARCH = new URLArtifactEventType(12,
 			getBundle().getString("WebTypes.webSearch.name"), // NON-NLS
 			WEB_ACTIVITY,
@@ -226,11 +281,11 @@ public SortedSet<TimelineEventType> getSubTypes() {
 				final BlackboardAttribute subject = getAttributeSafe(artf, new Type(TSK_SUBJECT));
 				BlackboardAttribute phoneNumber = getAttributeSafe(artf, new Type(TSK_PHONE_NUMBER));
 				// Make our best effort to find a valid phoneNumber for the description
-				if( phoneNumber == null) {
+				if (phoneNumber == null) {
 					phoneNumber = getAttributeSafe(artf, new Type(TSK_PHONE_NUMBER_TO));
 				}
-				
-				if( phoneNumber == null) {
+
+				if (phoneNumber == null) {
 					phoneNumber = getAttributeSafe(artf, new Type(TSK_PHONE_NUMBER_FROM));
 				}
 
@@ -281,13 +336,13 @@ public SortedSet<TimelineEventType> getSubTypes() {
 			new AttributeExtractor(new Type(TSK_NAME)),
 			artf -> {
 				BlackboardAttribute phoneNumber = getAttributeSafe(artf, new Type(TSK_PHONE_NUMBER));
-				if( phoneNumber == null) {
+				if (phoneNumber == null) {
 					phoneNumber = getAttributeSafe(artf, new Type(TSK_PHONE_NUMBER_TO));
 				}
-				if( phoneNumber == null) {
+				if (phoneNumber == null) {
 					phoneNumber = getAttributeSafe(artf, new Type(TSK_PHONE_NUMBER_FROM));
 				}
-				
+
 				return stringValueOf(phoneNumber);
 			},
 			new AttributeExtractor(new Type(TSK_DIRECTION)));
@@ -298,16 +353,22 @@ public SortedSet<TimelineEventType> getSubTypes() {
 			new BlackboardArtifact.Type(TSK_EMAIL_MSG),
 			new Type(TSK_DATETIME_SENT),
 			artf -> {
-				final BlackboardAttribute emailFrom = getAttributeSafe(artf, new Type(TSK_EMAIL_FROM));
-				final BlackboardAttribute emailTo = getAttributeSafe(artf, new Type(TSK_EMAIL_TO));
-				return stringValueOf(emailFrom) + " to " + stringValueOf(emailTo); // NON-NLS
+				String emailFrom = stringValueOf(getAttributeSafe(artf, new Type(TSK_EMAIL_FROM)));
+				if (emailFrom.length() > TimelineEventArtifactTypeImpl.EMAIL_TO_FROM_LENGTH_MAX) {
+					emailFrom = emailFrom.substring(0, TimelineEventArtifactTypeImpl.EMAIL_TO_FROM_LENGTH_MAX);
+				}
+				String emailTo = stringValueOf(getAttributeSafe(artf, new Type(TSK_EMAIL_TO)));
+				if (emailTo.length() > TimelineEventArtifactTypeImpl.EMAIL_TO_FROM_LENGTH_MAX) {
+					emailTo = emailTo.substring(0, TimelineEventArtifactTypeImpl.EMAIL_TO_FROM_LENGTH_MAX);
+				}
+				return emailFrom + " to " + emailTo; // NON-NLS
 			},
 			new AttributeExtractor(new Type(TSK_SUBJECT)),
 			artf -> {
 				final BlackboardAttribute msgAttribute = getAttributeSafe(artf, new Type(TSK_EMAIL_CONTENT_PLAIN));
 				String msg = stringValueOf(msgAttribute);
-				if (msg.length() > EMAIL_FULL_DESCRIPTION_LENGTH_MAX) {
-					msg = msg.substring(0, EMAIL_FULL_DESCRIPTION_LENGTH_MAX);
+				if (msg.length() > TimelineEventArtifactTypeImpl.EMAIL_FULL_DESCRIPTION_LENGTH_MAX) {
+					msg = msg.substring(0, TimelineEventArtifactTypeImpl.EMAIL_FULL_DESCRIPTION_LENGTH_MAX);
 				}
 				return msg;
 			});
@@ -350,9 +411,9 @@ public SortedSet<TimelineEventType> getSubTypes() {
 	//custom event type base type
 	TimelineEventType CUSTOM_TYPES = new TimelineEventTypeImpl(22,
 			getBundle().getString("BaseTypes.customTypes.name"), // NON-NLS
-			TypeLevel.BASE_TYPE, ROOT_EVENT_TYPE) {
+			HierarchyLevel.CATEGORY, ROOT_EVENT_TYPE) {
 		@Override
-		public SortedSet< TimelineEventType> getSubTypes() {
+		public SortedSet< TimelineEventType> getChildren() {
 			return ImmutableSortedSet.of(OTHER, USER_CREATED);
 		}
 	};
@@ -387,7 +448,7 @@ public SortedSet< TimelineEventType> getSubTypes() {
 			new BlackboardArtifact.Type(TSK_TL_EVENT),
 			new BlackboardAttribute.Type(TSK_DATETIME),
 			new BlackboardAttribute.Type(TSK_DESCRIPTION));
-	
+
 	TimelineEventType WEB_FORM_AUTOFILL = new TimelineEventArtifactTypeImpl(27,
 			getBundle().getString("WebTypes.webFormAutoFill.name"),//NON-NLS
 			WEB_ACTIVITY,
@@ -399,7 +460,7 @@ public SortedSet< TimelineEventType> getSubTypes() {
 				final BlackboardAttribute count = getAttributeSafe(artf, new Type(TSK_COUNT));
 				return stringValueOf(name) + ":" + stringValueOf(value) + " count: " + stringValueOf(count); // NON-NLS
 			}, new EmptyExtractor(), new EmptyExtractor());
-	
+
 	TimelineEventType WEB_FORM_ADDRESSES = new URLArtifactEventType(28,
 			getBundle().getString("WebTypes.webFormAddress.name"),//NON-NLS
 			WEB_ACTIVITY,
@@ -407,20 +468,20 @@ public SortedSet< TimelineEventType> getSubTypes() {
 			new Type(TSK_DATETIME_ACCESSED),
 			new Type(TSK_EMAIL));
 
-	static SortedSet<? extends TimelineEventType> getBaseTypes() {
-		return ROOT_EVENT_TYPE.getSubTypes();
+	static SortedSet<? extends TimelineEventType> getCategoryTypes() {
+		return ROOT_EVENT_TYPE.getChildren();
 	}
 
 	static SortedSet<? extends TimelineEventType> getFileSystemTypes() {
-		return FILE_SYSTEM.getSubTypes();
+		return FILE_SYSTEM.getChildren();
 	}
 
 	static SortedSet<? extends TimelineEventType> getWebActivityTypes() {
-		return WEB_ACTIVITY.getSubTypes();
+		return WEB_ACTIVITY.getChildren();
 	}
 
 	static SortedSet<? extends TimelineEventType> getMiscTypes() {
-		return MISC_TYPES.getSubTypes();
+		return MISC_TYPES.getChildren();
 	}
 
 	static String stringValueOf(BlackboardAttribute attr) {
diff --git a/bindings/java/src/org/sleuthkit/datamodel/TimelineEventTypeImpl.java b/bindings/java/src/org/sleuthkit/datamodel/TimelineEventTypeImpl.java
index 4f0b228117ebf3aacc0785785142a12edaaeb370..08a63323d60ee1c878d83b12b8aae1ce098f5b9a 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/TimelineEventTypeImpl.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/TimelineEventTypeImpl.java
@@ -24,44 +24,42 @@
 import org.apache.commons.lang3.ObjectUtils;
 
 /**
- * Implementation of TimelineEventType for the standard predefined event types AND has package
- scope parsing methods.
+ * Implementation of TimelineEventType for the standard predefined event types.
  */
 class TimelineEventTypeImpl implements TimelineEventType {
 
 	private final long typeID;
 	private final String displayName;
 	private final TimelineEventType superType;
-	private final TimelineEventType.TypeLevel eventTypeZoomLevel;
+	private final TimelineEventType.HierarchyLevel eventTypeZoomLevel;
 
 	/**
-	 * 
-	 * @param typeID  ID (from the Database)
+	 *
+	 * @param typeID             ID (from the Database)
 	 * @param displayName
 	 * @param eventTypeZoomLevel Where it is in the type hierarchy
-	 * @param superType 
+	 * @param superType
 	 */
-	TimelineEventTypeImpl(long typeID, String displayName, TimelineEventType.TypeLevel eventTypeZoomLevel, TimelineEventType superType) {
+	TimelineEventTypeImpl(long typeID, String displayName, TimelineEventType.HierarchyLevel eventTypeZoomLevel, TimelineEventType superType) {
 		this.superType = superType;
 		this.typeID = typeID;
 		this.displayName = displayName;
 		this.eventTypeZoomLevel = eventTypeZoomLevel;
 	}
 
-	
 	TimelineEventDescription parseDescription(String fullDescriptionRaw, String medDescriptionRaw, String shortDescriptionRaw) {
 		// The standard/default implementation:  Just bundle the three description levels into one object.
 		return new TimelineEventDescription(fullDescriptionRaw, medDescriptionRaw, shortDescriptionRaw);
 	}
 
 	@Override
-	public SortedSet<? extends TimelineEventType> getSubTypes() {
+	public SortedSet<? extends TimelineEventType> getChildren() {
 		return ImmutableSortedSet.of();
 	}
 
 	@Override
-	public Optional<? extends TimelineEventType> getSubType(String string) {
-		return getSubTypes().stream()
+	public Optional<? extends TimelineEventType> getChild(String string) {
+		return getChildren().stream()
 				.filter(type -> type.getDisplayName().equalsIgnoreCase(displayName))
 				.findFirst();
 	}
@@ -72,13 +70,13 @@ public String getDisplayName() {
 	}
 
 	@Override
-	public TimelineEventType getSuperType() {
+	public TimelineEventType getParent() {
 		return ObjectUtils.defaultIfNull(superType, ROOT_EVENT_TYPE);
 
 	}
 
 	@Override
-	public TimelineEventType.TypeLevel getTypeLevel() {
+	public TimelineEventType.HierarchyLevel getTypeHierarchyLevel() {
 		return eventTypeZoomLevel;
 	}
 
diff --git a/bindings/java/src/org/sleuthkit/datamodel/TimelineEventTypes.java b/bindings/java/src/org/sleuthkit/datamodel/TimelineEventTypes.java
index bd74899822b22ee493a33db7e9cf618d4de999e9..e2544d22855881d9ddd4d94db542c6edfc448f67 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/TimelineEventTypes.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/TimelineEventTypes.java
@@ -91,7 +91,7 @@ TimelineEventDescription parseDescription(String fullDescriptionRaw, String medD
 
 	static class FilePathEventType extends TimelineEventTypeImpl {
 
-		FilePathEventType(long typeID, String displayName, TimelineEventType.TypeLevel eventTypeZoomLevel, TimelineEventType superType) {
+		FilePathEventType(long typeID, String displayName, TimelineEventType.HierarchyLevel eventTypeZoomLevel, TimelineEventType superType) {
 			super(typeID, displayName, eventTypeZoomLevel, superType);
 		}
 
diff --git a/bindings/java/src/org/sleuthkit/datamodel/TimelineFilter.java b/bindings/java/src/org/sleuthkit/datamodel/TimelineFilter.java
index c28e7c3a1e24ea97f2a88107946b11ba32d5a733..cec25c9d4461d706a7dd061d09fda032c84bfc52 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/TimelineFilter.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/TimelineFilter.java
@@ -1,7 +1,7 @@
 /*
  * Sleuth Kit Data Model
  *
- * Copyright 2018 Basis Technology Corp.
+ * Copyright 2018-2019 Basis Technology Corp.
  * Contact: carrier <at> sleuthkit <dot> org
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -41,15 +41,16 @@
 import static org.sleuthkit.datamodel.SleuthkitCase.escapeSingleQuotes;
 
 /**
- * Interface for timeline event filters. Filters are given to the
- * TimelineManager who interpretes them appropriately for all db queries.
+ * An interface for timeline events filters used to selectively query the
+ * timeline tables in the case database for timeline events via the APIs of the
+ * timeline manager.
  */
 public abstract class TimelineFilter {
 
 	/**
-	 * get the display name of this filter
+	 * Gets the display name for this filter.
 	 *
-	 * @return a name for this filter to show in the UI
+	 * @return The display name.
 	 */
 	public abstract String getDisplayName();
 
@@ -64,6 +65,11 @@ public abstract class TimelineFilter {
 	 */
 	abstract String getSQLWhere(TimelineManager manager);
 
+	/**
+	 * Makes a copy of this filter.
+	 *
+	 * @return A copy of this filter.
+	 */
 	public abstract TimelineFilter copyOf();
 
 	@SuppressWarnings("unchecked")
@@ -73,21 +79,28 @@ static <S extends TimelineFilter, T extends CompoundFilter<S>> T copySubFilters(
 	}
 
 	/**
-	 * Intersection (And) filter
+	 * A timeline events filter that ANDs together a collection of timeline
+	 * event filters.
 	 *
-	 * @param <S> The type of sub Filters in this IntersectionFilter.
+	 * @param <SubFilterType> The type of the filters to be AND'ed together.
 	 */
-	public static class IntersectionFilter<S extends TimelineFilter> extends CompoundFilter<S> {
+	public static class IntersectionFilter<SubFilterType extends TimelineFilter> extends CompoundFilter<SubFilterType> {
 
+		/**
+		 * Constructs timeline events filter that ANDs together a collection of
+		 * timeline events filters.
+		 *
+		 * @param subFilters The collection of filters to be AND'ed together.
+		 */
 		@VisibleForTesting
-		public IntersectionFilter(List<S> subFilters) {
+		public IntersectionFilter(List<SubFilterType> subFilters) {
 			super(subFilters);
 		}
 
 		@Override
-		public IntersectionFilter<S> copyOf() {
+		public IntersectionFilter<SubFilterType> copyOf() {
 			@SuppressWarnings("unchecked")
-			List<S> subfilters = Lists.transform(getSubFilters(), f -> (S) f.copyOf()); //make copies of all the subfilters.
+			List<SubFilterType> subfilters = Lists.transform(getSubFilters(), f -> (SubFilterType) f.copyOf()); //make copies of all the subfilters.
 			return new IntersectionFilter<>(subfilters);
 		}
 
@@ -109,68 +122,83 @@ String getSQLWhere(TimelineManager manager) {
 					.collect(Collectors.joining(" AND "));
 			return join.isEmpty() ? trueLiteral : "(" + join + ")";
 		}
+
 	}
 
 	/**
-	 * Event Type Filter. An instance of EventTypeFilter is usually a tree that
-	 * parallels the event type hierarchy with one filter/node for each event
-	 * type.
+	 * A timeline events filter used to query for a subset of the event types in
+	 * the event types hierarchy. The filter is built via a recursive descent
+	 * from any given type in the hierarchy, effectively creating a filter that
+	 * accepts the events in a branch of the event types hierarchy.
 	 */
 	public static final class EventTypeFilter extends UnionFilter<EventTypeFilter> {
 
-		/**
-		 * the event type this filter passes
-		 */
-		private final TimelineEventType eventType;
+		private final TimelineEventType rootEventType;
 
 		/**
-		 * private constructor that enables non recursive/tree construction of
-		 * the filter hierarchy for use in EventTypeFilter.copyOf().
+		 * Constucts a timeline events filter used to query for a subset of the
+		 * event types in the event types hierarchy. The filter is optionally
+		 * built via a recursive descent from any given type in the hierarchy,
+		 * effectively creating a filter that accepts the events in a branch of
+		 * the event types hierarchy. Thsi constructor exists solely for the use
+		 * of this filter's implementation of the copyOf API.
 		 *
-		 * @param eventType the event type this filter passes
-		 * @param recursive true if subfilters should be added for each subtype.
-		 *                  False if no subfilters should be added.
+		 * @param rootEventType The "root" of the event hierarchy for the
+		 *                      purposes of this filter.
+		 * @param recursive     Whether or not to do a recursive descent of the
+		 *                      event types hierarchy from the root event type.
 		 */
-		private EventTypeFilter(TimelineEventType eventType, boolean recursive) {
+		private EventTypeFilter(TimelineEventType rootEventType, boolean recursive) {
 			super(FXCollections.observableArrayList());
-			this.eventType = eventType;
+			this.rootEventType = rootEventType;
 			if (recursive) {
 				// add subfilters for each subtype
-				for (TimelineEventType subType : eventType.getSubTypes()) {
+				for (TimelineEventType subType : rootEventType.getChildren()) {
 					addSubFilter(new EventTypeFilter(subType));
 				}
 			}
 		}
 
 		/**
-		 * public constructor. creates a subfilter for each subtype of the given
-		 * event type
+		 * Constructs a timeline events filter used to query for a subset of the
+		 * event types in the event types hierarchy. The subset of event types
+		 * that pass the filter is determined by a recursive descent from any
+		 * given type in the hierarchy, effectively creating a filter that
+		 * accepts the events in a branch of the event types hierarchy.
 		 *
-		 * @param eventType the event type this filter will pass
+		 * @param rootEventType The "root" of the event hierarchy for the
+		 *                      purposes of this filter.
 		 */
-		public EventTypeFilter(TimelineEventType eventType) {
-			this(eventType, true);
+		public EventTypeFilter(TimelineEventType rootEventType) {
+			this(rootEventType, true);
 		}
 
-		public TimelineEventType getEventType() {
-			return eventType;
+		/**
+		 * Gets the "root" of the branch of the event types hierarchy accepted
+		 * by this filter.
+		 *
+		 * @return The "root" event type.
+		 */
+		public TimelineEventType getRootEventType() {
+			return rootEventType;
 		}
 
 		@Override
 		public String getDisplayName() {
-			return (TimelineEventType.ROOT_EVENT_TYPE.equals(eventType)) ? BundleProvider.getBundle().getString("TypeFilter.displayName.text") : eventType.getDisplayName();
+			return (TimelineEventType.ROOT_EVENT_TYPE.equals(rootEventType)) ? BundleProvider.getBundle().getString("TypeFilter.displayName.text") : rootEventType.getDisplayName();
 		}
 
 		@Override
 		public EventTypeFilter copyOf() {
 			//make a nonrecursive copy of this filter, and then copy subfilters
-			return copySubFilters(this, new EventTypeFilter(eventType, false));
+			// RC (10/1/19): Why?
+			return copySubFilters(this, new EventTypeFilter(rootEventType, false));
 		}
 
 		@Override
 		public int hashCode() {
 			int hash = 7;
-			hash = 17 * hash + Objects.hashCode(this.eventType);
+			hash = 17 * hash + Objects.hashCode(this.rootEventType);
 			return hash;
 		}
 
@@ -186,7 +214,7 @@ public boolean equals(Object obj) {
 				return false;
 			}
 			final EventTypeFilter other = (EventTypeFilter) obj;
-			if (notEqual(this.eventType, other.eventType)) {
+			if (notEqual(this.rootEventType, other.getRootEventType())) {
 				return false;
 			}
 			return Objects.equals(this.getSubFilters(), other.getSubFilters());
@@ -199,7 +227,7 @@ String getSQLWhere(TimelineManager manager) {
 
 		private Stream<String> getSubTypeIDs() {
 			if (this.getSubFilters().isEmpty()) {
-				return Stream.of(String.valueOf(getEventType().getTypeID()));
+				return Stream.of(String.valueOf(getRootEventType().getTypeID()));
 			} else {
 				return this.getSubFilters().stream().flatMap(EventTypeFilter::getSubTypeIDs);
 			}
@@ -207,49 +235,60 @@ private Stream<String> getSubTypeIDs() {
 
 		@Override
 		public String toString() {
-			return "EventTypeFilter{" + "eventType=" + eventType + ", subfilters=" + getSubFilters() + '}';
+			return "EventTypeFilter{" + "rootEventType=" + rootEventType + ", subfilters=" + getSubFilters() + '}';
 		}
 
 	}
 
 	/**
-	 * Filter to show only events that are associated with objects that have 
-	 * file or result tags.
+	 * A timeline events filter used to query for events where the direct source
+	 * (file or artifact) of the events has either been tagged or not tagged.
 	 */
 	public static final class TagsFilter extends TimelineFilter {
-		private final BooleanProperty booleanProperty = new SimpleBooleanProperty();
+
+		private final BooleanProperty eventSourcesAreTagged = new SimpleBooleanProperty();
 
 		/**
-		 * Filter constructor.
+		 * Constructs a timeline events filter used to query for a events where
+		 * the direct source (file or artifact) of the events has not been
+		 * tagged.
 		 */
-		public TagsFilter() {}
+		public TagsFilter() {
+		}
 
 		/**
-		 * Filter constructor and set initial state.
-		 * 
-		 * @param isTagged Boolean initial state for the filter.
+		 * Constructs a timeline events filter used to query for events where
+		 * the direct source (file or artifact) of the events has either been
+		 * tagged or not tagged.
+		 *
+		 * @param eventSourceIsTagged Whether the direct sources of the events
+		 *                            need to be tagged or not tagged to be
+		 *                            accepted by this filter.
 		 */
-		public TagsFilter(boolean isTagged) {
-			booleanProperty.set(isTagged);
+		public TagsFilter(boolean eventSourceIsTagged) {
+			this.eventSourcesAreTagged.set(eventSourceIsTagged);
 		}
 
 		/**
-		 * Set the state of the filter.
-		 * 
-		 * @param isTagged True to filter events that are associated tagged items
-		 * or results
+		 * Sets whether the direct sources of the events have to be tagged or
+		 * not tagged to be accepted by this filter.
+		 *
+		 * @param eventSourceIsTagged Whether the direct sources of the events
+		 *                            have to be tagged or not tagged to be
+		 *                            accepted by this filter.
 		 */
-		public synchronized void setTagged(boolean isTagged) {
-			booleanProperty.set(isTagged);
+		public synchronized void setEventSourcesAreTagged(boolean eventSourceIsTagged) {
+			this.eventSourcesAreTagged.set(eventSourceIsTagged);
 		}
 
 		/**
-		 * Returns the current state of this filter.
-		 * 
-		 * @return True to filter by objects that are tagged.
+		 * Indicates whether the direct sources of the events have to be tagged
+		 * or not tagged.
+		 *
+		 * @return True or false.
 		 */
-		public synchronized boolean isTagged() {
-			return booleanProperty.get();
+		public synchronized boolean getEventSourceAreTagged() {
+			return eventSourcesAreTagged.get();
 		}
 
 		@Override
@@ -259,7 +298,7 @@ public String getDisplayName() {
 
 		@Override
 		public TagsFilter copyOf() {
-			return new TagsFilter(booleanProperty.get());
+			return new TagsFilter(eventSourcesAreTagged.get());
 		}
 
 		@Override
@@ -268,33 +307,35 @@ public boolean equals(Object obj) {
 				return false;
 			}
 
-			return ((TagsFilter)obj).isTagged() == booleanProperty.get();
+			return ((TagsFilter) obj).getEventSourceAreTagged() == eventSourcesAreTagged.get();
 		}
 
 		@Override
 		public int hashCode() {
 			int hash = 7;
-			hash = 67 * hash + Objects.hashCode(this.booleanProperty);
+			hash = 67 * hash + Objects.hashCode(this.eventSourcesAreTagged);
 			return hash;
 		}
-		
+
 		@Override
 		String getSQLWhere(TimelineManager manager) {
-			String whereStr = "";
-			if	(booleanProperty.get()) {
+			String whereStr;
+			if (eventSourcesAreTagged.get()) {
 				whereStr = "tagged = 1";
 			} else {
 				whereStr = "tagged = 0";
 			}
-			
+
 			return whereStr;
 		}
+
 	}
 
 	/**
-	 * Union(or) filter
+	 * A timeline events filter that ORs together a collection of timeline
+	 * events filters.
 	 *
-	 * @param <SubFilterType> The type of the subfilters.
+	 * @param <SubFilterType> The type of the filters to be OR'ed together.
 	 */
 	public static abstract class UnionFilter<SubFilterType extends TimelineFilter> extends TimelineFilter.CompoundFilter<SubFilterType> {
 
@@ -322,23 +363,43 @@ String getSQLWhere(TimelineManager manager) {
 	}
 
 	/**
-	 * Filter for text matching
+	 * A timeline events filter used to query for events that have a particular
+	 * substring in their short, medium, or full descriptions.
 	 */
 	public static final class TextFilter extends TimelineFilter {
 
-		private final SimpleStringProperty textProperty = new SimpleStringProperty();
+		private final SimpleStringProperty descriptionSubstring = new SimpleStringProperty();
 
+		/**
+		 * Constructs a timeline events filter used to query for events that
+		 * have the empty string as a substring in their short, medium, or full
+		 * descriptions.
+		 */
 		public TextFilter() {
 			this("");
 		}
 
-		public TextFilter(String text) {
+		/**
+		 * Constructs a timeline events filter used to query for events that
+		 * have a given substring in their short, medium, or full descriptions.
+		 *
+		 * @param descriptionSubstring The substring that must be present in one
+		 *                             or more of the descriptions of each event
+		 *                             that passes the filter.
+		 */
+		public TextFilter(String descriptionSubstring) {
 			super();
-			this.textProperty.set(text.trim());
+			this.descriptionSubstring.set(descriptionSubstring.trim());
 		}
 
-		public synchronized void setText(String text) {
-			this.textProperty.set(text.trim());
+		/**
+		 * Sets the substring that must be present in one or more of the
+		 * descriptions of each event that passes the filter.
+		 *
+		 * @param descriptionSubstring The substring.
+		 */
+		public synchronized void setDescriptionSubstring(String descriptionSubstring) {
+			this.descriptionSubstring.set(descriptionSubstring.trim());
 		}
 
 		@Override
@@ -346,17 +407,29 @@ public String getDisplayName() {
 			return BundleProvider.getBundle().getString("TextFilter.displayName.text");
 		}
 
-		public synchronized String getText() {
-			return textProperty.getValue();
+		/**
+		 * Gets the substring that must be present in one or more of the
+		 * descriptions of each event that passes the filter.
+		 *
+		 * @return The required substring.
+		 */
+		public synchronized String getSubstring() {
+			return descriptionSubstring.getValue();
 		}
 
-		public Property<String> textProperty() {
-			return textProperty;
+		/**
+		 * Gets the substring that must be present in one or more of the
+		 * descriptions of each event that passes the filter.
+		 *
+		 * @return The required substring as a Property.
+		 */
+		public Property<String> substringProperty() {
+			return descriptionSubstring;
 		}
 
 		@Override
 		public synchronized TextFilter copyOf() {
-			return new TextFilter(getText());
+			return new TextFilter(getSubstring());
 		}
 
 		@Override
@@ -368,22 +441,22 @@ public boolean equals(Object obj) {
 				return false;
 			}
 			final TextFilter other = (TextFilter) obj;
-			return Objects.equals(getText(), other.getText());
+			return Objects.equals(getSubstring(), other.getSubstring());
 		}
 
 		@Override
 		public int hashCode() {
 			int hash = 5;
-			hash = 29 * hash + Objects.hashCode(this.textProperty.get());
+			hash = 29 * hash + Objects.hashCode(this.descriptionSubstring.get());
 			return hash;
 		}
 
 		@Override
 		String getSQLWhere(TimelineManager manager) {
-			if (StringUtils.isNotBlank(this.getText())) {
-				return "((med_description like '%" + escapeSingleQuotes(this.getText()) + "%')" //NON-NLS
-						+ " or (full_description like '%" + escapeSingleQuotes(this.getText()) + "%')" //NON-NLS
-						+ " or (short_description like '%" + escapeSingleQuotes(this.getText()) + "%'))"; //NON-NLS
+			if (StringUtils.isNotBlank(this.getSubstring())) {
+				return "((med_description like '%" + escapeSingleQuotes(this.getSubstring()) + "%')" //NON-NLS
+						+ " or (full_description like '%" + escapeSingleQuotes(this.getSubstring()) + "%')" //NON-NLS
+						+ " or (short_description like '%" + escapeSingleQuotes(this.getSubstring()) + "%'))"; //NON-NLS
 			} else {
 				return manager.getSQLWhere(null);
 			}
@@ -391,109 +464,174 @@ String getSQLWhere(TimelineManager manager) {
 
 		@Override
 		public String toString() {
-			return "TextFilter{" + "textProperty=" + textProperty.getValue() + '}';
+			return "TextFilter{" + "textProperty=" + descriptionSubstring.getValue() + '}';
 		}
 
 	}
 
 	/**
-	 * An implementation of IntersectionFilter designed to be used as the root
-	 * of a filter tree. provides named access to specific subfilters.
+	 * A timeline events filter that ANDs together instances of a variety of
+	 * event filter types to create what is in effect a "tree" of filters.
 	 */
 	public static final class RootFilter extends IntersectionFilter<TimelineFilter> {
 
-		private final HideKnownFilter knownFilter;
+		private final HideKnownFilter knownFilesFilter;
 		private final TagsFilter tagsFilter;
-		private final HashHitsFilter hashFilter;
-		private final TextFilter textFilter;
-		private final EventTypeFilter typeFilter;
+		private final HashHitsFilter hashSetHitsFilter;
+		private final TextFilter descriptionSubstringFilter;
+		private final EventTypeFilter eventTypesFilter;
 		private final DataSourcesFilter dataSourcesFilter;
 		private final FileTypesFilter fileTypesFilter;
-		private final Set<TimelineFilter> namedSubFilters = new HashSet<>();
+		private final Set<TimelineFilter> additionalFilters = new HashSet<>();
 
+		/**
+		 * Get the data sources filter of this filter.
+		 *
+		 * @return The filter.
+		 */
 		public DataSourcesFilter getDataSourcesFilter() {
 			return dataSourcesFilter;
 		}
 
+		/**
+		 * Gets the tagged events sources filter of this filter.
+		 *
+		 * @return The filter.
+		 */
 		public TagsFilter getTagsFilter() {
 			return tagsFilter;
 		}
 
+		/**
+		 * Gets the source file hash set hits filter of this filter.
+		 *
+		 * @return The filter.
+		 */
 		public HashHitsFilter getHashHitsFilter() {
-			return hashFilter;
+			return hashSetHitsFilter;
 		}
 
+		/**
+		 * Gets the event types filter of this filter.
+		 *
+		 * @return The filter.
+		 */
 		public EventTypeFilter getEventTypeFilter() {
-			return typeFilter;
+			return eventTypesFilter;
 		}
 
+		/**
+		 * Gets the exclude known source files filter of this filter.
+		 *
+		 * @return The filter.
+		 */
 		public HideKnownFilter getKnownFilter() {
-			return knownFilter;
+			return knownFilesFilter;
 		}
 
+		/**
+		 * Gets the description substring filter of this filter.
+		 *
+		 * @return The filter.
+		 */
 		public TextFilter getTextFilter() {
-			return textFilter;
+			return descriptionSubstringFilter;
 		}
 
+		/**
+		 * Gets the source file types filter of this filter.
+		 *
+		 * @return The filter.
+		 */
 		public FileTypesFilter getFileTypesFilter() {
 			return fileTypesFilter;
 		}
 
-		public RootFilter(HideKnownFilter knownFilter, TagsFilter tagsFilter, HashHitsFilter hashFilter,
-				TextFilter textFilter, EventTypeFilter typeFilter, DataSourcesFilter dataSourcesFilter,
-				FileTypesFilter fileTypesFilter, Collection<TimelineFilter> annonymousSubFilters) {
-			super(FXCollections.observableArrayList(textFilter, knownFilter, tagsFilter, dataSourcesFilter, hashFilter, fileTypesFilter, typeFilter));
-
+		/**
+		 * Constructs a timeline events filter that ANDs together instances of a
+		 * variety of event filter types to create what is in effect a "tree" of
+		 * filters.
+		 *
+		 * @param knownFilesFilter           A filter that excludes events with
+		 *                                   knwon file event sources.
+		 * @param tagsFilter                 A filter that exludes or includes
+		 *                                   events with tagged event sources.
+		 * @param hashSetHitsFilter          A filter that excludes or includes
+		 *                                   events with event sources that have
+		 *                                   hash set hits.
+		 * @param descriptionSubstringFilter A filter that requires a substring
+		 *                                   to be present in the event
+		 *                                   description.
+		 * @param eventTypesFilter           A filter that accepts events of
+		 *                                   specified events types.
+		 * @param dataSourcesFilter          A filter that accepts events
+		 *                                   associated with a specified subset
+		 *                                   of data sources.
+		 * @param fileTypesFilter            A filter that includes or excludes
+		 *                                   events with source files of
+		 *                                   particular media types.
+		 * @param additionalFilters          Additional filters.
+		 */
+		public RootFilter(
+				HideKnownFilter knownFilesFilter,
+				TagsFilter tagsFilter,
+				HashHitsFilter hashSetHitsFilter,
+				TextFilter descriptionSubstringFilter,
+				EventTypeFilter eventTypesFilter,
+				DataSourcesFilter dataSourcesFilter,
+				FileTypesFilter fileTypesFilter,
+				Collection<TimelineFilter> additionalFilters) {
+
+			super(FXCollections.observableArrayList(descriptionSubstringFilter, knownFilesFilter, tagsFilter, dataSourcesFilter, hashSetHitsFilter, fileTypesFilter, eventTypesFilter));
 			getSubFilters().removeIf(Objects::isNull);
-			this.knownFilter = knownFilter;
+			this.knownFilesFilter = knownFilesFilter;
 			this.tagsFilter = tagsFilter;
-			this.hashFilter = hashFilter;
-			this.textFilter = textFilter;
-			this.typeFilter = typeFilter;
+			this.hashSetHitsFilter = hashSetHitsFilter;
+			this.descriptionSubstringFilter = descriptionSubstringFilter;
+			this.eventTypesFilter = eventTypesFilter;
 			this.dataSourcesFilter = dataSourcesFilter;
 			this.fileTypesFilter = fileTypesFilter;
-
-			namedSubFilters.addAll(asList(textFilter, knownFilter, tagsFilter, dataSourcesFilter, hashFilter, fileTypesFilter, typeFilter));
-			namedSubFilters.removeIf(Objects::isNull);
-			annonymousSubFilters.stream().
+			this.additionalFilters.addAll(asList(descriptionSubstringFilter, knownFilesFilter, tagsFilter, dataSourcesFilter, hashSetHitsFilter, fileTypesFilter, eventTypesFilter));
+			this.additionalFilters.removeIf(Objects::isNull);
+			additionalFilters.stream().
 					filter(Objects::nonNull).
-					filter(this::isNotNamedSubFilter).
+					filter(this::hasAdditionalFilter).
 					map(TimelineFilter::copyOf).
 					forEach(anonymousFilter -> getSubFilters().add(anonymousFilter));
 		}
 
 		@Override
 		public RootFilter copyOf() {
-			Set<TimelineFilter> annonymousSubFilters = getSubFilters().stream()
-					.filter(this::isNotNamedSubFilter)
+			Set<TimelineFilter> subFilters = getSubFilters().stream()
+					.filter(this::hasAdditionalFilter)
 					.map(TimelineFilter::copyOf)
 					.collect(Collectors.toSet());
-			return new RootFilter(knownFilter.copyOf(), tagsFilter.copyOf(),
-					hashFilter.copyOf(), textFilter.copyOf(), typeFilter.copyOf(),
-					dataSourcesFilter.copyOf(), fileTypesFilter.copyOf(), annonymousSubFilters);
+			return new RootFilter(knownFilesFilter.copyOf(), tagsFilter.copyOf(),
+					hashSetHitsFilter.copyOf(), descriptionSubstringFilter.copyOf(), eventTypesFilter.copyOf(),
+					dataSourcesFilter.copyOf(), fileTypesFilter.copyOf(), subFilters);
 
 		}
 
-		private boolean isNotNamedSubFilter(TimelineFilter subFilter) {
-			return !(namedSubFilters.contains(subFilter));
+		private boolean hasAdditionalFilter(TimelineFilter subFilter) {
+			return !(additionalFilters.contains(subFilter));
 		}
 
 		@Override
 		public String toString() {
-			return "RootFilter{" + "knownFilter=" + knownFilter + ", tagsFilter=" + tagsFilter + ", hashFilter=" + hashFilter + ", textFilter=" + textFilter + ", typeFilter=" + typeFilter + ", dataSourcesFilter=" + dataSourcesFilter + ", fileTypesFilter=" + fileTypesFilter + ", namedSubFilters=" + namedSubFilters + '}';
+			return "RootFilter{" + "knownFilter=" + knownFilesFilter + ", tagsFilter=" + tagsFilter + ", hashFilter=" + hashSetHitsFilter + ", textFilter=" + descriptionSubstringFilter + ", typeFilter=" + eventTypesFilter + ", dataSourcesFilter=" + dataSourcesFilter + ", fileTypesFilter=" + fileTypesFilter + ", namedSubFilters=" + additionalFilters + '}';
 		}
 
 		@Override
 		public int hashCode() {
 			int hash = 7;
-			hash = 17 * hash + Objects.hashCode(this.knownFilter);
+			hash = 17 * hash + Objects.hashCode(this.knownFilesFilter);
 			hash = 17 * hash + Objects.hashCode(this.tagsFilter);
-			hash = 17 * hash + Objects.hashCode(this.hashFilter);
-			hash = 17 * hash + Objects.hashCode(this.textFilter);
-			hash = 17 * hash + Objects.hashCode(this.typeFilter);
+			hash = 17 * hash + Objects.hashCode(this.hashSetHitsFilter);
+			hash = 17 * hash + Objects.hashCode(this.descriptionSubstringFilter);
+			hash = 17 * hash + Objects.hashCode(this.eventTypesFilter);
 			hash = 17 * hash + Objects.hashCode(this.dataSourcesFilter);
 			hash = 17 * hash + Objects.hashCode(this.fileTypesFilter);
-			hash = 17 * hash + Objects.hashCode(this.namedSubFilters);
+			hash = 17 * hash + Objects.hashCode(this.additionalFilters);
 			return hash;
 		}
 
@@ -509,35 +647,36 @@ public boolean equals(Object obj) {
 				return false;
 			}
 			final RootFilter other = (RootFilter) obj;
-			if (notEqual(this.knownFilter, other.knownFilter)) {
+			if (notEqual(this.knownFilesFilter, other.getKnownFilter())) {
 				return false;
 			}
-			if (notEqual(this.tagsFilter, other.tagsFilter)) {
+			if (notEqual(this.tagsFilter, other.getTagsFilter())) {
 				return false;
 			}
-			if (notEqual(this.hashFilter, other.hashFilter)) {
+			if (notEqual(this.hashSetHitsFilter, other.getHashHitsFilter())) {
 				return false;
 			}
-			if (notEqual(this.textFilter, other.textFilter)) {
+			if (notEqual(this.descriptionSubstringFilter, other.getTextFilter())) {
 				return false;
 			}
-			if (notEqual(this.typeFilter, other.typeFilter)) {
+			if (notEqual(this.eventTypesFilter, other.getEventTypeFilter())) {
 				return false;
 			}
-			if (notEqual(this.dataSourcesFilter, other.dataSourcesFilter)) {
+			if (notEqual(this.dataSourcesFilter, other.getDataSourcesFilter())) {
 				return false;
 			}
 
-			if (notEqual(this.fileTypesFilter, other.fileTypesFilter)) {
+			if (notEqual(this.fileTypesFilter, other.getFileTypesFilter())) {
 				return false;
 			}
-			return Objects.equals(this.namedSubFilters, other.namedSubFilters);
+			return Objects.equals(this.additionalFilters, other.getSubFilters());
 		}
 
 	}
 
 	/**
-	 * Filter to hide known files
+	 * A timeline events filter used to filter out events that have a direct or
+	 * indirect event source that is a known file.
 	 */
 	public static final class HideKnownFilter extends TimelineFilter {
 
@@ -546,10 +685,6 @@ public String getDisplayName() {
 			return BundleProvider.getBundle().getString("hideKnownFilter.displayName.text");
 		}
 
-		public HideKnownFilter() {
-			super();
-		}
-
 		@Override
 		public HideKnownFilter copyOf() {
 			return new HideKnownFilter();
@@ -577,11 +712,13 @@ String getSQLWhere(TimelineManager manager) {
 		public String toString() {
 			return "HideKnownFilter{" + '}';
 		}
+
 	}
 
 	/**
-	 * A Filter with a collection of sub-filters. Concrete implementations can
-	 * decide how to combine the sub-filters.
+	 * A timeline events filter composed of a collection of event filters.
+	 * Concrete implementations can decide how to combine the filters in the
+	 * collection.
 	 *
 	 * @param <SubFilterType> The type of the subfilters.
 	 */
@@ -593,23 +730,31 @@ protected void addSubFilter(SubFilterType subfilter) {
 			}
 		}
 
-		/**
-		 * The list of sub-filters that make up this filter
-		 */
 		private final ObservableList<SubFilterType> subFilters = FXCollections.observableArrayList();
 
+		/**
+		 * Gets the collection of filters that make up this filter.
+		 *
+		 * @return The filters.
+		 */
 		public final ObservableList<SubFilterType> getSubFilters() {
 			return subFilters;
 		}
 
+		/**
+		 * Indicates whether or not this filter has subfilters.
+		 *
+		 * @return True or false.
+		 */
 		public boolean hasSubFilters() {
 			return getSubFilters().isEmpty() == false;
 		}
 
 		/**
-		 * construct a compound filter from a list of other filters to combine.
+		 * Constructs a timeline events filter composed of a collection of event
+		 * filters.
 		 *
-		 * @param subFilters
+		 * @param subFilters The collection of filters.
 		 */
 		protected CompoundFilter(List<SubFilterType> subFilters) {
 			super();
@@ -647,23 +792,41 @@ public String toString() {
 		}
 
 	}
-	
+
 	/**
-	 * Filter for an individual datasource
+	 * A timeline events filter used to query for events associated with a given
+	 * data source.
 	 */
 	public static final class DataSourceFilter extends TimelineFilter {
 
 		private final String dataSourceName;
 		private final long dataSourceID;
 
+		/**
+		 * Gets the object ID of the specified data source.
+		 *
+		 * @return The data source object ID.
+		 */
 		public long getDataSourceID() {
 			return dataSourceID;
 		}
 
+		/**
+		 * Gets the display name of the specified data source.
+		 *
+		 * @return The data source display name.
+		 */
 		public String getDataSourceName() {
 			return dataSourceName;
 		}
 
+		/**
+		 * Constructs a timeline events filter used to query for events
+		 * associated with a given data source.
+		 *
+		 * @param dataSourceName The data source display name.
+		 * @param dataSourceID   The data source object ID.
+		 */
 		public DataSourceFilter(String dataSourceName, long dataSourceID) {
 			super();
 			this.dataSourceName = dataSourceName;
@@ -714,41 +877,53 @@ String getSQLWhere(TimelineManager manager) {
 	}
 
 	/**
-	 * TimelineFilter for events that are associated with objects have Hash Hits.
+	 * A timeline events filter used to query for events where the files that
+	 * are the direct or indirect sources of the events either have or do not
+	 * have hash set hits.
+	 *
 	 */
 	public static final class HashHitsFilter extends TimelineFilter {
-		private final BooleanProperty booleanProperty = new SimpleBooleanProperty();
+
+		private final BooleanProperty eventSourcesHaveHashSetHits = new SimpleBooleanProperty();
 
 		/**
-		 * Default constructor.
+		 * Constructs a timeline events filter used to query for events where
+		 * the files that are the direct or indirect sources of the events
+		 * either do not have hash set hits.
 		 */
-		public HashHitsFilter() {}
+		public HashHitsFilter() {
+		}
 
 		/**
-		 * Construct the hash hit filter and set state based given argument.
-		 * 
-		 * @param hasHashHit True to filter items that have hash hits.
+		 * Constructs a timeline events filter used to query for events where
+		 * the files that are the direct or indirect sources of the events
+		 * either have or do not have hash set hits.
+		 *
+		 * @param hasHashHit Whether or not the files associated with the events
+		 *                   have or do not have hash set hits.
 		 */
 		public HashHitsFilter(boolean hasHashHit) {
-			booleanProperty.set(hasHashHit);
+			eventSourcesHaveHashSetHits.set(hasHashHit);
 		}
 
 		/**
-		 * Set the state of the filter.
-		 * 
-		 * @param hasHashHit True to filter by items that have hash hits.
+		 * Sets whether or not the files associated with the events have or do
+		 * not have hash set hits
+		 *
+		 * @param hasHashHit True or false.
 		 */
-		public synchronized void setTagged(boolean hasHashHit) {
-			booleanProperty.set(hasHashHit);
+		public synchronized void setEventSourcesHaveHashSetHits(boolean hasHashHit) {
+			eventSourcesHaveHashSetHits.set(hasHashHit);
 		}
 
 		/**
-		 * Returns the current state of the filter.
-		 * 
-		 * @return True to filter by hash hits
+		 * Indicates whether or not the files associated with the events have or
+		 * do not have hash set hits
+		 *
+		 * @return True or false.
 		 */
-		public synchronized boolean hasHashHits() {
-			return booleanProperty.get();
+		public synchronized boolean getEventSourcesHaveHashSetHits() {
+			return eventSourcesHaveHashSetHits.get();
 		}
 
 		@Override
@@ -758,7 +933,7 @@ public String getDisplayName() {
 
 		@Override
 		public HashHitsFilter copyOf() {
-			return new HashHitsFilter(booleanProperty.get());
+			return new HashHitsFilter(eventSourcesHaveHashSetHits.get());
 		}
 
 		@Override
@@ -767,37 +942,36 @@ public boolean equals(Object obj) {
 				return false;
 			}
 
-			return ((HashHitsFilter)obj).hasHashHits() == booleanProperty.get();
+			return ((HashHitsFilter) obj).getEventSourcesHaveHashSetHits() == eventSourcesHaveHashSetHits.get();
 		}
 
 		@Override
 		public int hashCode() {
 			int hash = 7;
-			hash = 67 * hash + Objects.hashCode(this.booleanProperty);
+			hash = 67 * hash + Objects.hashCode(this.eventSourcesHaveHashSetHits);
 			return hash;
 		}
-		
+
 		@Override
 		String getSQLWhere(TimelineManager manager) {
 			String whereStr = "";
-			if	(booleanProperty.get()) {
+			if (eventSourcesHaveHashSetHits.get()) {
 				whereStr = "hash_hit = 1";
 			} else {
 				whereStr = "hash_hit = 0";
 			}
-			
+
 			return whereStr;
 		}
+
 	}
 
 	/**
-	 * union of DataSourceFilters
+	 * A timeline events filter used to query for events associated with a given
+	 * subset of data sources. The filter is a union of one or more single data
+	 * source filters.
 	 */
-	static public final class DataSourcesFilter extends UnionFilter< DataSourceFilter> {
-
-		public DataSourcesFilter() {
-			super();
-		}
+	static public final class DataSourcesFilter extends UnionFilter<DataSourceFilter> {
 
 		@Override
 		public DataSourcesFilter copyOf() {
@@ -808,10 +982,13 @@ public DataSourcesFilter copyOf() {
 		public String getDisplayName() {
 			return BundleProvider.getBundle().getString("DataSourcesFilter.displayName.text");
 		}
+
 	}
 
 	/**
-	 * union of FileTypeFilters
+	 * A timeline events filter used to query for events with direct or indirect
+	 * event sources that are files with a given set of media types. The filter
+	 * is a union of one or more file source filters.
 	 */
 	static public final class FileTypesFilter extends UnionFilter<FileTypeFilter> {
 
@@ -827,41 +1004,52 @@ public String getDisplayName() {
 		}
 
 	}
-	
+
 	/**
-     * Gets all files that are NOT the specified types
-     */
-    static public class InverseFileTypeFilter extends FileTypeFilter {
+	 * A timeline events filter used to query for events with direct or indirect
+	 * event sources that are files that do not have a given set of media types.
+	 */
+	static public class InverseFileTypeFilter extends FileTypeFilter {
 
-        public InverseFileTypeFilter(String displayName, Collection<String> mediaTypes) {
-            super(displayName, mediaTypes);
-        }
+		public InverseFileTypeFilter(String displayName, Collection<String> mediaTypes) {
+			super(displayName, mediaTypes);
+		}
 
-        @Override
-        public InverseFileTypeFilter copyOf() {
-            return new InverseFileTypeFilter(getDisplayName(), super.mediaTypes);
-        }
+		@Override
+		public InverseFileTypeFilter copyOf() {
+			return new InverseFileTypeFilter(getDisplayName(), super.mediaTypes);
+		}
 
-        @Override
-        String getSQLWhere(TimelineManager manager) {
-            return " NOT " + super.getSQLWhere(manager);
-        }
-    }
+		@Override
+		String getSQLWhere(TimelineManager manager) {
+			return " NOT " + super.getSQLWhere(manager);
+		}
+	}
 
 	/**
-	 * Filter for events derived from files with the given media/mime-types.
+	 * A timeline events filter used to query for events with direct or indirect
+	 * event sources that are files with a given set of media types.
 	 */
 	public static class FileTypeFilter extends TimelineFilter {
 
 		private final String displayName;
 		private final String sqlWhere;
-		Collection <String> mediaTypes = new HashSet<>();
+		Collection<String> mediaTypes = new HashSet<>();
 
 		private FileTypeFilter(String displayName, String sql) {
 			this.displayName = displayName;
 			this.sqlWhere = sql;
 		}
 
+		/**
+		 * Constructs a timeline events filter used to query for events with
+		 * direct or indirect event sources that are files with a given set of
+		 * media types.
+		 *
+		 * @param displayName The display name for the filter.
+		 * @param mediaTypes  The event source file media types that pass the
+		 *                    filter.
+		 */
 		public FileTypeFilter(String displayName, Collection<String> mediaTypes) {
 			this(displayName,
 					mediaTypes.stream()
@@ -924,4 +1112,5 @@ public String toString() {
 		}
 
 	}
+
 }
diff --git a/bindings/java/src/org/sleuthkit/datamodel/TimelineLevelOfDetail.java b/bindings/java/src/org/sleuthkit/datamodel/TimelineLevelOfDetail.java
new file mode 100755
index 0000000000000000000000000000000000000000..a0f7b8dc329e1709c2213fa54fa3e4f7e83e6b42
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/TimelineLevelOfDetail.java
@@ -0,0 +1,80 @@
+/*
+ * Sleuth Kit Data Model
+ *
+ * Copyright 2019 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.ResourceBundle;
+
+/**
+ * An enumeration of the levels of detail of various aspects of timeline data.
+ */
+public enum TimelineLevelOfDetail {
+
+	LOW(ResourceBundle.getBundle("org.sleuthkit.datamodel.Bundle").getString("TimelineLevelOfDetail.low")),
+	MEDIUM(ResourceBundle.getBundle("org.sleuthkit.datamodel.Bundle").getString("TimelineLevelOfDetail.medium")),
+	HIGH(ResourceBundle.getBundle("org.sleuthkit.datamodel.Bundle").getString("TimelineLevelOfDetail.high"));
+
+	private final String displayName;
+
+	/**
+	 * Gets the display name of this level of detail.
+	 *
+	 * @return The display name.
+	 */
+	public String getDisplayName() {
+		return displayName;
+	}
+
+	/**
+	 * Constructs an element of the enumeration of the levels of detail of
+	 * various aspects of timeline data such as event descriptions and the
+	 * timeline event types hierarchy.
+	 *
+	 * @param displayName The display name of the level of detail.
+	 */
+	private TimelineLevelOfDetail(String displayName) {
+		this.displayName = displayName;
+	}
+
+	/**
+	 * Gets the next higher level of detail relative to this level of detail.
+	 *
+	 * @return The next higher level of detail, may be null.
+	 */
+	public TimelineLevelOfDetail moreDetailed() {
+		try {
+			return values()[ordinal() + 1];
+		} catch (ArrayIndexOutOfBoundsException e) {
+			return null;
+		}
+	}
+
+	/**
+	 * Gets the next lower level of detail relative to this level of detail.
+	 *
+	 * @return The next lower level of detail, may be null.
+	 */
+	public TimelineLevelOfDetail lessDetailed() {
+		try {
+			return values()[ordinal() - 1];
+		} catch (ArrayIndexOutOfBoundsException e) {
+			return null;
+		}
+	}
+
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/TimelineManager.java b/bindings/java/src/org/sleuthkit/datamodel/TimelineManager.java
index 45f1189d204c4febdc7ba9f33da645cc5652fb2c..f633d6f71670b5afebb718c596d9c73bf653e2f6 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/TimelineManager.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/TimelineManager.java
@@ -1,7 +1,7 @@
 /*
  * Sleuth Kit Data Model
  *
- * Copyright 2013-2019 Basis Technology Corp.
+ * Copyright 2018-2019 Basis Technology Corp.
  * Contact: carrier <at> sleuthkit <dot> org
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,6 +18,7 @@
  */
 package org.sleuthkit.datamodel;
 
+import com.google.common.annotations.Beta;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import java.sql.PreparedStatement;
@@ -35,7 +36,6 @@
 import static java.util.Objects.isNull;
 import java.util.Optional;
 import java.util.Set;
-import java.util.logging.Logger;
 import java.util.stream.Collectors;
 import org.joda.time.DateTimeZone;
 import org.joda.time.Interval;
@@ -47,17 +47,14 @@
 import static org.sleuthkit.datamodel.StringUtils.buildCSVString;
 
 /**
- * Provides access to the Timeline features of SleuthkitCase
+ * Provides access to the timeline data in a case database.
  */
 public final class TimelineManager {
 
-	private static final Logger logger = Logger.getLogger(TimelineManager.class.getName());
-
 	/**
-	 * These event types are added to the DB in c++ land, but still need to be
-	 * put in the eventTypeIDMap
+	 * Timeline event types added to the case database when it is created.
 	 */
-	private static final ImmutableList<TimelineEventType> ROOT_BASE_AND_FILESYSTEM_TYPES
+	private static final ImmutableList<TimelineEventType> ROOT_CATEGORY_AND_FILESYSTEM_TYPES
 			= ImmutableList.of(
 					TimelineEventType.ROOT_EVENT_TYPE,
 					TimelineEventType.WEB_ACTIVITY,
@@ -69,61 +66,79 @@ public final class TimelineManager {
 					TimelineEventType.FILE_MODIFIED);
 
 	/**
-	 * These event types are predefined but not added to the DB by the C++ code.
-	 * They are added by the TimelineManager constructor.
+	 * Timeline event types added to the case database by the TimelineManager
+	 * constructor. Adding these types at runtime permits new child types of the
+	 * category types to be defined without modifying the table creation and
+	 * population code in the Sleuth Kit.
 	 */
 	private static final ImmutableList<TimelineEventType> PREDEFINED_EVENT_TYPES
 			= new ImmutableList.Builder<TimelineEventType>()
 					.add(TimelineEventType.CUSTOM_TYPES)
-					.addAll(TimelineEventType.WEB_ACTIVITY.getSubTypes())
-					.addAll(TimelineEventType.MISC_TYPES.getSubTypes())
-					.addAll(TimelineEventType.CUSTOM_TYPES.getSubTypes())
+					.addAll(TimelineEventType.WEB_ACTIVITY.getChildren())
+					.addAll(TimelineEventType.MISC_TYPES.getChildren())
+					.addAll(TimelineEventType.CUSTOM_TYPES.getChildren())
 					.build();
 
-	private final SleuthkitCase sleuthkitCase;
+	private final SleuthkitCase caseDB;
 
 	/**
-	 * map from event type id to TimelineEventType object.
+	 * Mapping of timeline event type IDs to TimelineEventType objects.
 	 */
 	private final Map<Long, TimelineEventType> eventTypeIDMap = new HashMap<>();
 
-	TimelineManager(SleuthkitCase tskCase) throws TskCoreException {
-		sleuthkitCase = tskCase;
+	/**
+	 * Constructs a timeline manager that provides access to the timeline data
+	 * in a case database.
+	 *
+	 * @param caseDB The case database.
+	 *
+	 * @throws TskCoreException If there is an error constructing the timeline
+	 *                          manager.
+	 */
+	TimelineManager(SleuthkitCase caseDB) throws TskCoreException {
+		this.caseDB = caseDB;
 
 		//initialize root and base event types, these are added to the DB in c++ land
-		ROOT_BASE_AND_FILESYSTEM_TYPES.forEach(eventType -> eventTypeIDMap.put(eventType.getTypeID(), eventType));
+		ROOT_CATEGORY_AND_FILESYSTEM_TYPES.forEach(eventType -> eventTypeIDMap.put(eventType.getTypeID(), eventType));
 
 		//initialize the other event types that aren't added in c++
-		sleuthkitCase.acquireSingleUserCaseWriteLock();
-		try (final CaseDbConnection con = sleuthkitCase.getConnection();
+		caseDB.acquireSingleUserCaseWriteLock();
+		try (final CaseDbConnection con = caseDB.getConnection();
 				final Statement statement = con.createStatement()) {
 			for (TimelineEventType type : PREDEFINED_EVENT_TYPES) {
 				con.executeUpdate(statement,
 						insertOrIgnore(" INTO tsk_event_types(event_type_id, display_name, super_type_id) "
 								+ "VALUES( " + type.getTypeID() + ", '"
 								+ escapeSingleQuotes(type.getDisplayName()) + "',"
-								+ type.getSuperType().getTypeID()
+								+ type.getParent().getTypeID()
 								+ ")")); //NON-NLS
 				eventTypeIDMap.put(type.getTypeID(), type);
 			}
 		} catch (SQLException ex) {
-			throw new TskCoreException("Failed to initialize event types.", ex); // NON-NLS
+			throw new TskCoreException("Failed to initialize timeline event types", ex); // NON-NLS
 		} finally {
-			sleuthkitCase.releaseSingleUserCaseWriteLock();
+			caseDB.releaseSingleUserCaseWriteLock();
 		}
 	}
 
-	SleuthkitCase getSleuthkitCase() {
-		return sleuthkitCase;
-	}
-
+	/**
+	 * Gets the smallest possible time interval that spans a collection of
+	 * timeline events.
+	 *
+	 * @param eventIDs The event IDs of the events for which to obtain the
+	 *                 spanning interval.
+	 *
+	 * @return The minimal spanning interval, may be null.
+	 *
+	 * @throws TskCoreException If there is an error querying the case database.
+	 */
 	public Interval getSpanningInterval(Collection<Long> eventIDs) throws TskCoreException {
 		if (eventIDs.isEmpty()) {
 			return null;
 		}
-		final String query = "SELECT Min(time) as minTime, Max(time) as maxTime FROM tsk_events WHERE event_id IN (" + buildCSVString(eventIDs) + ")";//NON-NLS
-		sleuthkitCase.acquireSingleUserCaseReadLock();
-		try (CaseDbConnection con = sleuthkitCase.getConnection();
+		final String query = "SELECT Min(time) as minTime, Max(time) as maxTime FROM tsk_events WHERE event_id IN (" + buildCSVString(eventIDs) + ")"; //NON-NLS
+		caseDB.acquireSingleUserCaseReadLock();
+		try (CaseDbConnection con = caseDB.getConnection();
 				Statement stmt = con.createStatement();
 				ResultSet results = stmt.executeQuery(query);) {
 			if (results.next()) {
@@ -132,22 +147,22 @@ public Interval getSpanningInterval(Collection<Long> eventIDs) throws TskCoreExc
 		} catch (SQLException ex) {
 			throw new TskCoreException("Error executing get spanning interval query: " + query, ex); // NON-NLS
 		} finally {
-			sleuthkitCase.releaseSingleUserCaseReadLock();
+			caseDB.releaseSingleUserCaseReadLock();
 		}
 		return null;
 	}
 
 	/**
-	 * Get the minimal interval that bounds all the vents that pass the given
-	 * filter.
+	 * Gets the smallest possible time interval that spans a collection of
+	 * timeline events.
 	 *
-	 * @param timeRange The time range that the events must be within.
-	 * @param filter    The filter that the events must pass.
-	 * @param timeZone  The timeZone to return the interval in.
+	 * @param timeRange A time range that the events must be within.
+	 * @param filter    A timeline events filter that the events must pass.
+	 * @param timeZone  The time zone for the returned time interval.
 	 *
-	 * @return The minimal interval that bounds the events.
+	 * @return The minimal spanning interval, may be null.
 	 *
-	 * @throws TskCoreException
+	 * @throws TskCoreException If there is an error querying the case database.
 	 */
 	public Interval getSpanningInterval(Interval timeRange, TimelineFilter.RootFilter filter, DateTimeZone timeZone) throws TskCoreException {
 		long start = timeRange.getStartMillis() / 1000;
@@ -158,8 +173,8 @@ public Interval getSpanningInterval(Interval timeRange, TimelineFilter.RootFilte
 				+ "			 WHERE time <=" + start + " AND " + sqlWhere + ") AS start,"
 				+ "		 (SELECT Min(time)  FROM " + augmentedEventsTablesSQL
 				+ "			 WHERE time >= " + end + " AND " + sqlWhere + ") AS end";//NON-NLS
-		sleuthkitCase.acquireSingleUserCaseReadLock();
-		try (CaseDbConnection con = sleuthkitCase.getConnection();
+		caseDB.acquireSingleUserCaseReadLock();
+		try (CaseDbConnection con = caseDB.getConnection();
 				Statement stmt = con.createStatement(); //can't use prepared statement because of complex where clause
 				ResultSet results = stmt.executeQuery(queryString);) {
 
@@ -168,23 +183,31 @@ public Interval getSpanningInterval(Interval timeRange, TimelineFilter.RootFilte
 				long end2 = results.getLong("end"); // NON-NLS
 
 				if (end2 == 0) {
-					end2 = getMaxTime();
+					end2 = getMaxEventTime();
 				}
 				return new Interval(start2 * 1000, (end2 + 1) * 1000, timeZone);
 			}
 		} catch (SQLException ex) {
 			throw new TskCoreException("Failed to get MIN time.", ex); // NON-NLS
 		} finally {
-			sleuthkitCase.releaseSingleUserCaseReadLock();
+			caseDB.releaseSingleUserCaseReadLock();
 		}
 		return null;
 	}
 
+	/**
+	 * Gets the timeline event with a given event ID.
+	 *
+	 * @param eventID An event ID.
+	 *
+	 * @return The timeline event, may be null.
+	 *
+	 * @throws TskCoreException If there is an error querying the case database.
+	 */
 	public TimelineEvent getEventById(long eventID) throws TskCoreException {
 		String sql = "SELECT * FROM  " + getAugmentedEventsTablesSQL(false) + " WHERE event_id = " + eventID;
-
-		sleuthkitCase.acquireSingleUserCaseReadLock();
-		try (CaseDbConnection con = sleuthkitCase.getConnection();
+		caseDB.acquireSingleUserCaseReadLock();
+		try (CaseDbConnection con = caseDB.getConnection();
 				Statement stmt = con.createStatement();) {
 			try (ResultSet results = stmt.executeQuery(sql);) {
 				if (results.next()) {
@@ -192,7 +215,7 @@ public TimelineEvent getEventById(long eventID) throws TskCoreException {
 					TimelineEventType type = getEventType(typeID).orElseThrow(() -> newEventTypeMappingException(typeID)); //NON-NLS
 					return new TimelineEvent(eventID,
 							results.getLong("data_source_obj_id"),
-							results.getLong("file_obj_id"),
+							results.getLong("content_obj_id"),
 							results.getLong("artifact_id"),
 							results.getLong("time"),
 							type, results.getString("full_description"),
@@ -203,24 +226,23 @@ public TimelineEvent getEventById(long eventID) throws TskCoreException {
 				}
 			}
 		} catch (SQLException sqlEx) {
-			throw new TskCoreException("exception while querying for event with id = " + eventID, sqlEx); // NON-NLS
+			throw new TskCoreException("Error while executing query " + sql, sqlEx); // NON-NLS
 		} finally {
-			sleuthkitCase.releaseSingleUserCaseReadLock();
+			caseDB.releaseSingleUserCaseReadLock();
 		}
 		return null;
 	}
 
 	/**
-	 * Get the IDs of all the events within the given time range that pass the
-	 * given filter.
+	 * Gets the event IDs of the timeline events within a given time range that
+	 * pass a given timeline events filter.
 	 *
-	 * @param timeRange The Interval that all returned events must be within.
-	 * @param filter    The Filter that all returned events must pass.
+	 * @param timeRange The time range that the events must be within.
+	 * @param filter    The timeline events filter that the events must pass.
 	 *
-	 * @return A List of event ids, sorted by timestamp of the corresponding
-	 *         event..
+	 * @return A list of event IDs ordered by event time.
 	 *
-	 * @throws org.sleuthkit.datamodel.TskCoreException
+	 * @throws TskCoreException If there is an error querying the case database.
 	 */
 	public List<Long> getEventIDs(Interval timeRange, TimelineFilter.RootFilter filter) throws TskCoreException {
 		Long startTime = timeRange.getStartMillis() / 1000;
@@ -234,8 +256,8 @@ public List<Long> getEventIDs(Interval timeRange, TimelineFilter.RootFilter filt
 
 		String query = "SELECT tsk_events.event_id AS event_id FROM " + getAugmentedEventsTablesSQL(filter)
 				+ " WHERE time >=  " + startTime + " AND time <" + endTime + " AND " + getSQLWhere(filter) + " ORDER BY time ASC"; // NON-NLS
-		sleuthkitCase.acquireSingleUserCaseReadLock();
-		try (CaseDbConnection con = sleuthkitCase.getConnection();
+		caseDB.acquireSingleUserCaseReadLock();
+		try (CaseDbConnection con = caseDB.getConnection();
 				Statement stmt = con.createStatement();
 				ResultSet results = stmt.executeQuery(query);) {
 			while (results.next()) {
@@ -243,87 +265,91 @@ public List<Long> getEventIDs(Interval timeRange, TimelineFilter.RootFilter filt
 			}
 
 		} catch (SQLException sqlEx) {
-			throw new TskCoreException("failed to execute query for event ids in range", sqlEx); // NON-NLS
+			throw new TskCoreException("Error while executing query " + query, sqlEx); // NON-NLS
 		} finally {
-			sleuthkitCase.releaseSingleUserCaseReadLock();
+			caseDB.releaseSingleUserCaseReadLock();
 		}
 
 		return resultIDs;
 	}
 
 	/**
-	 * @return maximum time in seconds from unix epoch
+	 * Gets the maximum timeline event time in the case database.
 	 *
-	 * @throws org.sleuthkit.datamodel.TskCoreException
+	 * @return The maximum timeline event time in seconds since the UNIX epoch,
+	 *         or -1 if there are no timeline events in the case database.
+	 *
+	 * @throws TskCoreException If there is an error querying the case database.
 	 */
-	public Long getMaxTime() throws TskCoreException {
-		sleuthkitCase.acquireSingleUserCaseReadLock();
-
-		try (CaseDbConnection con = sleuthkitCase.getConnection();
+	public Long getMaxEventTime() throws TskCoreException {
+		caseDB.acquireSingleUserCaseReadLock();
+		try (CaseDbConnection con = caseDB.getConnection();
 				Statement stms = con.createStatement();
 				ResultSet results = stms.executeQuery(STATEMENTS.GET_MAX_TIME.getSQL());) {
 			if (results.next()) {
 				return results.getLong("max"); // NON-NLS
 			}
 		} catch (SQLException ex) {
-			throw new TskCoreException("Failed to get MAX time.", ex); // NON-NLS
+			throw new TskCoreException("Error while executing query " + STATEMENTS.GET_MAX_TIME.getSQL(), ex); // NON-NLS
 		} finally {
-			sleuthkitCase.releaseSingleUserCaseReadLock();
+			caseDB.releaseSingleUserCaseReadLock();
 		}
 		return -1l;
 	}
 
 	/**
-	 * @return maximum time in seconds from unix epoch
+	 * Gets the minimum timeline event time in the case database.
+	 *
+	 * @return The minimum timeline event time in seconds since the UNIX epoch,
+	 *         or -1 if there are no timeline events in the case database.
 	 *
-	 * @throws org.sleuthkit.datamodel.TskCoreException
+	 * @throws TskCoreException If there is an error querying the case database.
 	 */
-	public Long getMinTime() throws TskCoreException {
-		sleuthkitCase.acquireSingleUserCaseReadLock();
-
-		try (CaseDbConnection con = sleuthkitCase.getConnection();
+	public Long getMinEventTime() throws TskCoreException {
+		caseDB.acquireSingleUserCaseReadLock();
+		try (CaseDbConnection con = caseDB.getConnection();
 				Statement stms = con.createStatement();
 				ResultSet results = stms.executeQuery(STATEMENTS.GET_MIN_TIME.getSQL());) {
 			if (results.next()) {
 				return results.getLong("min"); // NON-NLS
 			}
 		} catch (SQLException ex) {
-			throw new TskCoreException("Failed to get MIN time.", ex); // NON-NLS
+			throw new TskCoreException("Error while executing query " + STATEMENTS.GET_MAX_TIME.getSQL(), ex); // NON-NLS
 		} finally {
-			sleuthkitCase.releaseSingleUserCaseReadLock();
+			caseDB.releaseSingleUserCaseReadLock();
 		}
 		return -1l;
 	}
 
 	/**
-	 * Get an TimelineEventType object given it's ID.
+	 * Gets the timeline event type with a given event type ID.
 	 *
-	 * @param eventTypeID The ID of the event type to get.
+	 * @param eventTypeID An event type ID.
 	 *
-	 * @return An Optional containing the TimelineEventType, or an empty Optional if no
-         TimelineEventType with the given ID was found.
+	 * @return The timeline event type in an Optional object, may be empty if
+	 *         the event type is not found.
 	 */
 	public Optional<TimelineEventType> getEventType(long eventTypeID) {
 		return Optional.ofNullable(eventTypeIDMap.get(eventTypeID));
 	}
 
 	/**
-	 * Get a list of all the EventTypes.
+	 * Gets all of the timeline event types in the case database.
 	 *
-	 * @return A list of all the eventTypes.
+	 * @return A list of timeline event types.
 	 */
 	public ImmutableList<TimelineEventType> getEventTypes() {
 		return ImmutableList.copyOf(eventTypeIDMap.values());
 	}
 
 	private String insertOrIgnore(String query) {
-		switch (sleuthkitCase.getDatabaseType()) {
+		switch (caseDB.getDatabaseType()) {
 			case POSTGRESQL:
 				return " INSERT " + query + " ON CONFLICT DO NOTHING "; //NON-NLS
 			case SQLITE:
 				return " INSERT OR IGNORE " + query; //NON-NLS
 			default:
-				throw new UnsupportedOperationException("Unsupported DB type: " + sleuthkitCase.getDatabaseType().name());
+				throw new UnsupportedOperationException("Unsupported DB type: " + caseDB.getDatabaseType().name());
 		}
 	}
 
@@ -347,15 +373,14 @@ String getSQL() {
 	}
 
 	/**
-	 * Get a List of event IDs for the events that are derived from the given
-	 * artifact.
+	 * Gets a list of event IDs for the timeline events that have a given
+	 * artifact as the event source.
 	 *
-	 * @param artifact The BlackboardArtifact to get derived event IDs for.
+	 * @param artifact An artifact.
 	 *
-	 * @return A List of event IDs for the events that are derived from the
-	 *         given artifact.
+	 * @return The list of event IDs.
 	 *
-	 * @throws org.sleuthkit.datamodel.TskCoreException
+	 * @throws TskCoreException If there is an error querying the case database.
 	 */
 	public List<Long> getEventIDsForArtifact(BlackboardArtifact artifact) throws TskCoreException {
 		ArrayList<Long> eventIDs = new ArrayList<>();
@@ -364,8 +389,8 @@ public List<Long> getEventIDsForArtifact(BlackboardArtifact artifact) throws Tsk
 				= "SELECT event_id FROM tsk_events "
 				+ " LEFT JOIN tsk_event_descriptions on ( tsk_events.event_description_id = tsk_event_descriptions.event_description_id ) "
 				+ " WHERE artifact_id = " + artifact.getArtifactID();
-		sleuthkitCase.acquireSingleUserCaseReadLock();
-		try (CaseDbConnection con = sleuthkitCase.getConnection();
+		caseDB.acquireSingleUserCaseReadLock();
+		try (CaseDbConnection con = caseDB.getConnection();
 				Statement stmt = con.createStatement();
 				ResultSet results = stmt.executeQuery(query);) {
 			while (results.next()) {
@@ -374,30 +399,31 @@ public List<Long> getEventIDsForArtifact(BlackboardArtifact artifact) throws Tsk
 		} catch (SQLException ex) {
 			throw new TskCoreException("Error executing getEventIDsForArtifact query.", ex); // NON-NLS
 		} finally {
-			sleuthkitCase.releaseSingleUserCaseReadLock();
+			caseDB.releaseSingleUserCaseReadLock();
 		}
 		return eventIDs;
 	}
 
 	/**
-	 * Get a Set of event IDs for the events that are derived from the given
-	 * file.
+	 * Gets a list of event IDs for the timeline events that have a given
+	 * content as the event source.
 	 *
-	 * @param file                    The File / data source to get derived
-	 *                                event IDs for.
+	 * @param content                 The content.
 	 * @param includeDerivedArtifacts If true, also get event IDs for events
-	 *                                derived from artifacts derived form this
-	 *                                file. If false, only gets events derived
-	 *                                directly from this file (file system
-	 *                                timestamps).
+	 *                                where the event source is an artifact that
+	 *                                has the given content as its source.
 	 *
-	 * @return A Set of event IDs for the events that are derived from the given
-	 *         file.
+	 * @return The list of event IDs.
 	 *
-	 * @throws org.sleuthkit.datamodel.TskCoreException
+	 * @throws TskCoreException If there is an error querying the case database.
 	 */
-	public Set<Long> getEventIDsForFile(Content file, boolean includeDerivedArtifacts) throws TskCoreException {
-		return getEventAndDescriptionIDs(file.getId(), includeDerivedArtifacts).keySet();
+	public Set<Long> getEventIDsForContent(Content content, boolean includeDerivedArtifacts) throws TskCoreException {
+		caseDB.acquireSingleUserCaseWriteLock();
+		try (CaseDbConnection conn = caseDB.getConnection()) {
+			return getEventAndDescriptionIDs(conn, content.getId(), includeDerivedArtifacts).keySet();
+		} finally {
+			caseDB.releaseSingleUserCaseWriteLock();
+		}
 	}
 
 	/**
@@ -422,7 +448,7 @@ private long addEventDescription(long dataSourceObjId, long fileObjId, Long arti
 			boolean hasHashHits, boolean tagged, CaseDbConnection connection) throws TskCoreException {
 		String insertDescriptionSql
 				= "INSERT INTO tsk_event_descriptions ( "
-				+ "data_source_obj_id, file_obj_id, artifact_id,  "
+				+ "data_source_obj_id, content_obj_id, artifact_id,  "
 				+ " full_description, med_description, short_description, "
 				+ " hash_hit, tagged "
 				+ " ) VALUES ("
@@ -436,7 +462,7 @@ private long addEventDescription(long dataSourceObjId, long fileObjId, Long arti
 				+ booleanToInt(tagged)
 				+ " )";
 
-		sleuthkitCase.acquireSingleUserCaseWriteLock();
+		caseDB.acquireSingleUserCaseWriteLock();
 		try (Statement insertDescriptionStmt = connection.createStatement()) {
 			connection.executeUpdate(insertDescriptionStmt, insertDescriptionSql, PreparedStatement.RETURN_GENERATED_KEYS);
 			try (ResultSet generatedKeys = insertDescriptionStmt.getGeneratedKeys()) {
@@ -446,7 +472,7 @@ private long addEventDescription(long dataSourceObjId, long fileObjId, Long arti
 		} catch (SQLException ex) {
 			throw new TskCoreException("Failed to insert event description.", ex); // NON-NLS
 		} finally {
-			sleuthkitCase.releaseSingleUserCaseWriteLock();
+			caseDB.releaseSingleUserCaseWriteLock();
 		}
 	}
 
@@ -468,7 +494,7 @@ Collection<TimelineEvent> addEventsForNewFile(AbstractFile file, CaseDbConnectio
 		String description = file.getParentPath() + file.getName();
 		long fileObjId = file.getId();
 		Set<TimelineEvent> events = new HashSet<>();
-		sleuthkitCase.acquireSingleUserCaseWriteLock();
+		caseDB.acquireSingleUserCaseWriteLock();
 		try {
 			long descriptionID = addEventDescription(file.getDataSourceObjectId(), fileObjId, null,
 					description, null, null, false, false, connection);
@@ -490,11 +516,11 @@ Collection<TimelineEvent> addEventsForNewFile(AbstractFile file, CaseDbConnectio
 			}
 
 		} finally {
-			sleuthkitCase.releaseSingleUserCaseWriteLock();
+			caseDB.releaseSingleUserCaseWriteLock();
 		}
 		events.stream()
 				.map(TimelineEventAddedEvent::new)
-				.forEach(sleuthkitCase::fireTSKEvent);
+				.forEach(caseDB::fireTSKEvent);
 
 		return events;
 	}
@@ -530,8 +556,8 @@ Set<TimelineEvent> addArtifactEvents(BlackboardArtifact artifact) throws TskCore
 				eventType = eventTypeIDMap.getOrDefault(eventTypeID, TimelineEventType.OTHER);
 			}
 
-			// @@@ This casting is risky if we change class hierarchy, but was expediant.  Should move parsing to another class
-			addArtifactEvent(((TimelineEventArtifactTypeImpl)TimelineEventType.OTHER)::makeEventDescription, eventType, artifact)
+			// @@@ This casting is risky if we change class hierarchy, but was expedient.  Should move parsing to another class
+			addArtifactEvent(((TimelineEventArtifactTypeImpl) TimelineEventType.OTHER)::makeEventDescription, eventType, artifact)
 					.ifPresent(newEvents::add);
 		} else {
 			/*
@@ -551,7 +577,7 @@ Set<TimelineEvent> addArtifactEvents(BlackboardArtifact artifact) throws TskCore
 		}
 		newEvents.stream()
 				.map(TimelineEventAddedEvent::new)
-				.forEach(sleuthkitCase::fireTSKEvent);
+				.forEach(caseDB::fireTSKEvent);
 		return newEvents;
 	}
 
@@ -583,24 +609,24 @@ private Optional<TimelineEvent> addArtifactEvent(TSKCoreCheckedFunction<Blackboa
 		if (time <= 0) {
 			return Optional.empty();
 		}
-		String fullDescription = eventPayload.getFullDescription();
-		String medDescription = eventPayload.getMediumDescription();
-		String shortDescription = eventPayload.getShortDescription();
+		String fullDescription = eventPayload.getDescription(TimelineLevelOfDetail.HIGH);
+		String medDescription = eventPayload.getDescription(TimelineLevelOfDetail.MEDIUM);
+		String shortDescription = eventPayload.getDescription(TimelineLevelOfDetail.LOW);
 		long artifactID = artifact.getArtifactID();
 		long fileObjId = artifact.getObjectID();
 		long dataSourceObjectID = artifact.getDataSourceObjectID();
 
-		AbstractFile file = sleuthkitCase.getAbstractFileById(fileObjId);
+		AbstractFile file = caseDB.getAbstractFileById(fileObjId);
 		boolean hasHashHits = false;
 		// file will be null if source was data source or some non-file
 		if (file != null) {
 			hasHashHits = isNotEmpty(file.getHashSetNames());
 		}
-		boolean tagged = isNotEmpty(sleuthkitCase.getBlackboardArtifactTagsByArtifact(artifact));
+		boolean tagged = isNotEmpty(caseDB.getBlackboardArtifactTagsByArtifact(artifact));
 
 		TimelineEvent event;
-		sleuthkitCase.acquireSingleUserCaseWriteLock();
-		try (CaseDbConnection connection = getSleuthkitCase().getConnection();) {
+		caseDB.acquireSingleUserCaseWriteLock();
+		try (CaseDbConnection connection = caseDB.getConnection();) {
 
 			long descriptionID = addEventDescription(dataSourceObjectID, fileObjId, artifactID,
 					fullDescription, medDescription, shortDescription,
@@ -613,7 +639,7 @@ private Optional<TimelineEvent> addArtifactEvent(TSKCoreCheckedFunction<Blackboa
 					hasHashHits, tagged);
 
 		} finally {
-			sleuthkitCase.releaseSingleUserCaseWriteLock();
+			caseDB.releaseSingleUserCaseWriteLock();
 		}
 		return Optional.of(event);
 	}
@@ -623,7 +649,7 @@ private long addEventWithExistingDescription(Long time, TimelineEventType type,
 				= "INSERT INTO tsk_events ( event_type_id, event_description_id , time) "
 				+ " VALUES (" + type.getTypeID() + ", " + descriptionID + ", " + time + ")";
 
-		sleuthkitCase.acquireSingleUserCaseWriteLock();
+		caseDB.acquireSingleUserCaseWriteLock();
 		try (Statement insertRowStmt = connection.createStatement();) {
 			connection.executeUpdate(insertRowStmt, insertEventSql, PreparedStatement.RETURN_GENERATED_KEYS);
 
@@ -634,7 +660,7 @@ private long addEventWithExistingDescription(Long time, TimelineEventType type,
 		} catch (SQLException ex) {
 			throw new TskCoreException("Failed to insert event for existing description.", ex); // NON-NLS
 		} finally {
-			sleuthkitCase.releaseSingleUserCaseWriteLock();
+			caseDB.releaseSingleUserCaseWriteLock();
 		}
 	}
 
@@ -642,123 +668,181 @@ static private String quotePreservingNull(String value) {
 		return isNull(value) ? " NULL " : "'" + escapeSingleQuotes(value) + "'";//NON-NLS
 	}
 
+	private Map<Long, Long> getEventAndDescriptionIDs(CaseDbConnection conn, long contentObjID, boolean includeArtifacts) throws TskCoreException {
+		return getEventAndDescriptionIDsHelper(conn, contentObjID, (includeArtifacts ? "" : " AND artifact_id IS NULL"));
+	}
+
+	private Map<Long, Long> getEventAndDescriptionIDs(CaseDbConnection conn, long contentObjID, Long artifactID) throws TskCoreException {
+		return getEventAndDescriptionIDsHelper(conn, contentObjID, " AND artifact_id = " + artifactID);
+	}
+
+	private Map<Long, Long> getEventAndDescriptionIDsHelper(CaseDbConnection con, long fileObjID, String artifactClause) throws TskCoreException {
+		//map from event_id to the event_description_id for that event.
+		Map<Long, Long> eventIDToDescriptionIDs = new HashMap<>();
+		String sql = "SELECT event_id, tsk_events.event_description_id"
+				+ " FROM tsk_events "
+				+ " LEFT JOIN tsk_event_descriptions ON ( tsk_events.event_description_id = tsk_event_descriptions.event_description_id )"
+				+ " WHERE content_obj_id = " + fileObjID
+				+ artifactClause;
+		try (Statement selectStmt = con.createStatement(); ResultSet executeQuery = selectStmt.executeQuery(sql);) {
+			while (executeQuery.next()) {
+				eventIDToDescriptionIDs.put(executeQuery.getLong("event_id"), executeQuery.getLong("event_description_id")); //NON-NLS
+			}
+		} catch (SQLException ex) {
+			throw new TskCoreException("Error getting event description ids for object id = " + fileObjID, ex);
+		}
+		return eventIDToDescriptionIDs;
+	}
+
 	/**
-	 * Get events that are associated with the file
+	 * Finds all of the timeline events directly associated with a given content
+	 * and marks them as having an event source that is tagged. This does not
+	 * include timeline events where the event source is an artifact, even if
+	 * the artifact source is the tagged content.
 	 *
-	 * @param fileObjID
-	 * @param includeArtifacts true if results should also include events from
-	 *                         artifacts associated with the file.
+	 * @param content The content.
 	 *
-	 * @return A map from event_id to event_decsription_id.
+	 * @return The event IDs of the events that were marked as having a tagged
+	 *         event source.
 	 *
-	 * @throws TskCoreException
+	 * @throws TskCoreException If there is an error updating the case database.
+	 *
+	 * WARNING: THIS IS A BETA VERSION OF THIS METHOD, SUBJECT TO CHANGE AT ANY
+	 * TIME.
 	 */
-	private Map<Long, Long> getEventAndDescriptionIDs(long fileObjID, boolean includeArtifacts) throws TskCoreException {
-		return getEventAndDescriptionIDsHelper(fileObjID, (includeArtifacts ? "" : " AND artifact_id IS NULL"));
+	@Beta
+	public Set<Long> updateEventsForContentTagAdded(Content content) throws TskCoreException {
+		caseDB.acquireSingleUserCaseWriteLock();
+		try (CaseDbConnection conn = caseDB.getConnection()) {
+			Map<Long, Long> eventIDs = getEventAndDescriptionIDs(conn, content.getId(), false);
+			updateEventSourceTaggedFlag(conn, eventIDs.values(), 1);
+			return eventIDs.keySet();
+		} finally {
+			caseDB.releaseSingleUserCaseWriteLock();
+		}
 	}
 
 	/**
-	 * Get events that match both the file and artifact IDs
+	 * Finds all of the timeline events directly associated with a given content
+	 * and marks them as not having an event source that is tagged, if and only
+	 * if there are no other tags on the content. The inspection of events does
+	 * not include events where the event source is an artifact, even if the
+	 * artifact source is the content from which trhe tag was removed.
 	 *
-	 * @param fileObjID
-	 * @param artifactID
+	 * @param content The content.
 	 *
-	 * @return A map from event_id to event_decsription_id.
+	 * @return The event IDs of the events that were marked as not having a
+	 *         tagged event source.
 	 *
-	 * @throws TskCoreException
+	 * @throws TskCoreException If there is an error updating the case database.
+	 *
+	 * WARNING: THIS IS A BETA VERSION OF THIS METHOD, SUBJECT TO CHANGE AT ANY
+	 * TIME.
 	 */
-	private Map<Long, Long> getEventAndDescriptionIDs(long fileObjID, Long artifactID) throws TskCoreException {
-		return getEventAndDescriptionIDsHelper(fileObjID, " AND artifact_id = " + artifactID);
+	@Beta
+	public Set<Long> updateEventsForContentTagDeleted(Content content) throws TskCoreException {
+		caseDB.acquireSingleUserCaseWriteLock();
+		try (CaseDbConnection conn = caseDB.getConnection()) {
+			if (caseDB.getContentTagsByContent(content).isEmpty()) {
+				Map<Long, Long> eventIDs = getEventAndDescriptionIDs(conn, content.getId(), false);
+				updateEventSourceTaggedFlag(conn, eventIDs.values(), 0);
+				return eventIDs.keySet();
+			} else {
+				return Collections.emptySet();
+			}
+		} finally {
+			caseDB.releaseSingleUserCaseWriteLock();
+		}
 	}
 
 	/**
-	 * Get a map containging event_id and their corresponding
-	 * event_description_ids.
+	 * Finds all of the timeline events directly associated with a given
+	 * artifact and marks them as having an event source that is tagged.
 	 *
-	 * @param fileObjID      get event Ids for events that are derived from the
-	 *                       file with this id.
-	 * @param artifactClause SQL clause that clients can pass in to filter the
-	 *                       returned ids.
+	 * @param artifact The artifact.
 	 *
-	 * @return A map from event_id to event_decsription_id.
+	 * @return The event IDs of the events that were marked as having a tagged
+	 *         event source.
 	 *
-	 * @throws TskCoreException
+	 * @throws TskCoreException If there is an error updating the case database.
 	 */
-	private Map<Long, Long> getEventAndDescriptionIDsHelper(long fileObjID, String artifactClause) throws TskCoreException {
-		//map from event_id to the event_description_id for that event.
-		Map<Long, Long> eventIDToDescriptionIDs = new HashMap<>();
-		String sql = "SELECT event_id, tsk_events.event_description_id"
-				+ " FROM tsk_events "
-				+ " LEFT JOIN tsk_event_descriptions ON ( tsk_events.event_description_id = tsk_event_descriptions.event_description_id )"
-				+ " WHERE file_obj_id = " + fileObjID
-				+ artifactClause;
-
-		sleuthkitCase.acquireSingleUserCaseReadLock();
-		try (CaseDbConnection con = sleuthkitCase.getConnection();
-				Statement selectStmt = con.createStatement();
-				ResultSet executeQuery = selectStmt.executeQuery(sql);) {
-			while (executeQuery.next()) {
-				eventIDToDescriptionIDs.put(executeQuery.getLong("event_id"), executeQuery.getLong("event_description_id")); //NON-NLS
-			}
-		} catch (SQLException ex) {
-			throw new TskCoreException("Error getting event description ids for object id = " + fileObjID, ex);
+	public Set<Long> updateEventsForArtifactTagAdded(BlackboardArtifact artifact) throws TskCoreException {
+		caseDB.acquireSingleUserCaseWriteLock();
+		try (CaseDbConnection conn = caseDB.getConnection()) {
+			Map<Long, Long> eventIDs = getEventAndDescriptionIDs(conn, artifact.getObjectID(), artifact.getArtifactID());
+			updateEventSourceTaggedFlag(conn, eventIDs.values(), 1);
+			return eventIDs.keySet();
 		} finally {
-			sleuthkitCase.releaseSingleUserCaseReadLock();
+			caseDB.releaseSingleUserCaseWriteLock();
 		}
-		return eventIDToDescriptionIDs;
 	}
 
 	/**
-	 * Set any events with the given object and artifact ids as tagged.
+	 * Finds all of the timeline events directly associated with a given
+	 * artifact and marks them as not having an event source that is tagged, if
+	 * and only if there are no other tags on the artifact.
 	 *
-	 * @param fileObjId  the obj_id that this tag applies to, the id of the
-	 *                   content that the artifact is derived from for artifact
-	 *                   tags
-	 * @param artifactID the artifact_id that this tag applies to, or null if
-	 *                   this is a content tag
-	 * @param tagged     true to mark the matching events tagged, false to mark
-	 *                   them as untagged
+	 * @param artifact The artifact.
 	 *
-	 * @return the event ids that match the object/artifact pair.
+	 * @return The event IDs of the events that were marked as not having a
+	 *         tagged event source.
 	 *
-	 * @throws org.sleuthkit.datamodel.TskCoreException
+	 * @throws TskCoreException If there is an error updating the case database.
 	 */
-	public Set<Long> setEventsTagged(long fileObjId, Long artifactID, boolean tagged) throws TskCoreException {
-		sleuthkitCase.acquireSingleUserCaseWriteLock();
-		Map<Long, Long> eventIDs;  // map from event_ids to event_description_ids
-		if (Objects.isNull(artifactID)) {
-			eventIDs = getEventAndDescriptionIDs(fileObjId, false);
-		} else {
-			eventIDs = getEventAndDescriptionIDs(fileObjId, artifactID);
+	public Set<Long> updateEventsForArtifactTagDeleted(BlackboardArtifact artifact) throws TskCoreException {
+		caseDB.acquireSingleUserCaseWriteLock();
+		try (CaseDbConnection conn = caseDB.getConnection()) {
+			if (caseDB.getBlackboardArtifactTagsByArtifact(artifact).isEmpty()) {
+				Map<Long, Long> eventIDs = getEventAndDescriptionIDs(conn, artifact.getObjectID(), artifact.getArtifactID());
+				updateEventSourceTaggedFlag(conn, eventIDs.values(), 0);
+				return eventIDs.keySet();
+			} else {
+				return Collections.emptySet();
+			}
+		} finally {
+			caseDB.releaseSingleUserCaseWriteLock();
 		}
+	}
 
-		//update tagged state for all event with selected ids
-		try (CaseDbConnection con = sleuthkitCase.getConnection();
-				Statement updateStatement = con.createStatement();) {
-			updateStatement.executeUpdate("UPDATE tsk_event_descriptions SET tagged = " + booleanToInt(tagged)
-					+ " WHERE event_description_id IN (" + buildCSVString(eventIDs.values()) + ")"); //NON-NLS
+	private void updateEventSourceTaggedFlag(CaseDbConnection conn, Collection<Long> eventDescriptionIDs, int flagValue) throws TskCoreException {
+		String sql = "UPDATE tsk_event_descriptions SET tagged = " + flagValue + " WHERE event_description_id IN (" + buildCSVString(eventDescriptionIDs) + ")"; //NON-NLS
+		try (Statement updateStatement = conn.createStatement()) {
+			updateStatement.executeUpdate(sql);
 		} catch (SQLException ex) {
-			throw new TskCoreException("Error marking events tagged", ex);//NON-NLS
-		} finally {
-			sleuthkitCase.releaseSingleUserCaseWriteLock();
+			throw new TskCoreException("Error marking content events tagged: " + sql, ex);//NON-NLS
 		}
-		return eventIDs.keySet();
 	}
 
-	public Set<Long> setEventsHashed(long fileObjdId, boolean hashHits) throws TskCoreException {
-		sleuthkitCase.acquireSingleUserCaseWriteLock();
-		Map<Long, Long> eventIDs = getEventAndDescriptionIDs(fileObjdId, true);
-
-		try (CaseDbConnection con = sleuthkitCase.getConnection();
-				Statement updateStatement = con.createStatement();) {
-			updateStatement.executeUpdate("UPDATE tsk_event_descriptions SET hash_hit = " + booleanToInt(hashHits) //NON-NLS
-					+ " WHERE event_description_id IN (" + buildCSVString(eventIDs.values()) + ")"); //NON-NLS
+	/**
+	 * Finds all of the timeline events associated directly or indirectly with a
+	 * given content and marks them as having an event source that has a hash
+	 * set hit. This includes both the events that have the content as their
+	 * event source and the events for which the content is the source content
+	 * for the source artifact of the event.
+	 *
+	 * @param content The content.
+	 *
+	 * @return The event IDs of the events that were marked as having an event
+	 *         source with a hash set hit.
+	 *
+	 * @throws TskCoreException If there is an error updating the case database.
+	 */
+	public Set<Long> updateEventsForHashSetHit(Content content) throws TskCoreException {
+		caseDB.acquireSingleUserCaseWriteLock();
+		try (CaseDbConnection con = caseDB.getConnection(); Statement updateStatement = con.createStatement();) {
+			Map<Long, Long> eventIDs = getEventAndDescriptionIDs(con, content.getId(), true);
+			String sql = "UPDATE tsk_event_descriptions SET hash_hit = 1" + " WHERE event_description_id IN (" + buildCSVString(eventIDs.values()) + ")"; //NON-NLS
+			try {
+				updateStatement.executeUpdate(sql); //NON-NLS
+				return eventIDs.keySet();
+			} catch (SQLException ex) {
+				throw new TskCoreException("Error setting hash_hit of events.", ex);//NON-NLS
+			}
 		} catch (SQLException ex) {
 			throw new TskCoreException("Error setting hash_hit of events.", ex);//NON-NLS
 		} finally {
-			sleuthkitCase.releaseSingleUserCaseWriteLock();
+			caseDB.releaseSingleUserCaseWriteLock();
 		}
-		return eventIDs.keySet();
 	}
 
 	void rollBackTransaction(SleuthkitCase.CaseDbTransaction trans) throws TskCoreException {
@@ -766,35 +850,36 @@ void rollBackTransaction(SleuthkitCase.CaseDbTransaction trans) throws TskCoreEx
 	}
 
 	/**
-	 * Count all the events with the given options and return a map organizing
-	 * the counts in a hierarchy from date > eventtype> count
-	 *
-	 * @param startTime events before this time will be excluded (seconds from
-	 *                  unix epoch)
-	 * @param endTime   events at or after this time will be excluded (seconds
-	 *                  from unix epoch)
-	 * @param filter    only events that pass this filter will be counted
-	 * @param zoomLevel only events of this type or a subtype will be counted
-	 *                  and the counts will be organized into bins for each of
-	 *                  the subtypes of the given event type
-	 *
-	 * @return a map organizing the counts in a hierarchy from date > eventtype>
-	 *         count
-	 *
-	 * @throws org.sleuthkit.datamodel.TskCoreException
+	 * Counts the timeline events events that satisfy the given conditions.
+	 *
+	 * @param startTime         Events that occurred before this time are not
+	 *                          counted (units: seconds from UNIX epoch)
+	 * @param endTime           Events that occurred at or after this time are
+	 *                          not counted (seconds from unix epoch)
+	 * @param filter            Events that fall within the specified time range
+	 *                          are only ocunted if they pass this filter.
+	 * @param typeHierachyLevel Events that fall within the specified time range
+	 *                          and pass the specified filter asre only counted
+	 *                          if their types are at the specified level of the
+	 *                          event type hierarchy.
+	 *
+	 * @return The event counts for each event type at the specified level in
+	 *         the event types hierarchy.
+	 *
+	 * @throws TskCoreException If there is an error querying the case database.
 	 */
-	public Map<TimelineEventType, Long> countEventsByType(Long startTime, final Long endTime, TimelineFilter.RootFilter filter, TimelineEventType.TypeLevel zoomLevel) throws TskCoreException {
+	public Map<TimelineEventType, Long> countEventsByType(Long startTime, Long endTime, TimelineFilter.RootFilter filter, TimelineEventType.HierarchyLevel typeHierachyLevel) throws TskCoreException {
 		long adjustedEndTime = Objects.equals(startTime, endTime) ? endTime + 1 : endTime;
 		//do we want the base or subtype column of the databse
-		String typeColumn = typeColumnHelper(TimelineEventType.TypeLevel.SUB_TYPE.equals(zoomLevel));
+		String typeColumn = typeColumnHelper(TimelineEventType.HierarchyLevel.EVENT.equals(typeHierachyLevel));
 
 		String queryString = "SELECT count(DISTINCT tsk_events.event_id) AS count, " + typeColumn//NON-NLS
 				+ " FROM " + getAugmentedEventsTablesSQL(filter)//NON-NLS
 				+ " WHERE time >= " + startTime + " AND time < " + adjustedEndTime + " AND " + getSQLWhere(filter) // NON-NLS
 				+ " GROUP BY " + typeColumn; // NON-NLS
 
-		sleuthkitCase.acquireSingleUserCaseReadLock();
-		try (CaseDbConnection con = sleuthkitCase.getConnection();
+		caseDB.acquireSingleUserCaseReadLock();
+		try (CaseDbConnection con = caseDB.getConnection();
 				Statement stmt = con.createStatement();
 				ResultSet results = stmt.executeQuery(queryString);) {
 			Map<TimelineEventType, Long> typeMap = new HashMap<>();
@@ -809,7 +894,7 @@ public Map<TimelineEventType, Long> countEventsByType(Long startTime, final Long
 		} catch (SQLException ex) {
 			throw new TskCoreException("Error getting count of events from db: " + queryString, ex); // NON-NLS
 		} finally {
-			sleuthkitCase.releaseSingleUserCaseReadLock();
+			caseDB.releaseSingleUserCaseReadLock();
 		}
 	}
 
@@ -848,11 +933,23 @@ static private String getAugmentedEventsTablesSQL(TimelineFilter.RootFilter filt
 	 * @param needMimeTypes True if the filters require joining to the tsk_files
 	 *                      table for the mime_type.
 	 *
-	 * @return An SQL expression that produces an events table augmented with the
-	 *         columns required by the filters.
+	 * @return An SQL expression that produces an events table augmented with
+	 *         the columns required by the filters.
 	 */
 	static private String getAugmentedEventsTablesSQL(boolean needMimeTypes) {
-		return "( select event_id, time, tsk_event_descriptions.data_source_obj_id, file_obj_id, artifact_id, "
+		/*
+		 * Regarding the timeline event tables schema, note that several columns
+		 * in the tsk_event_descriptions table seem, at first glance, to be
+		 * attributes of events rather than their descriptions and would appear
+		 * to belong in tsk_events table instead. The rationale for putting the
+		 * data source object ID, content object ID, artifact ID and the flags
+		 * indicating whether or not the event source has a hash set hit or is
+		 * tagged were motivated by the fact that these attributes are identical
+		 * for each event in a set of file system file MAC time events. The
+		 * decision was made to avoid duplication and save space by placing this
+		 * data in the tsk_event-descriptions table.
+		 */
+		return "( SELECT event_id, time, tsk_event_descriptions.data_source_obj_id, content_obj_id, artifact_id, "
 				+ " full_description, med_description, short_description, tsk_events.event_type_id, super_type_id,"
 				+ " hash_hit, tagged "
 				+ (needMimeTypes ? ", mime_type" : "")
@@ -860,7 +957,7 @@ static private String getAugmentedEventsTablesSQL(boolean needMimeTypes) {
 				+ " JOIN tsk_event_descriptions ON ( tsk_event_descriptions.event_description_id = tsk_events.event_description_id)"
 				+ " JOIN tsk_event_types ON (tsk_events.event_type_id = tsk_event_types.event_type_id )  "
 				+ (needMimeTypes ? " LEFT OUTER JOIN tsk_files "
-						+ "	ON (tsk_event_descriptions.file_obj_id = tsk_files.obj_id)"
+						+ "	ON (tsk_event_descriptions.content_obj_id = tsk_files.obj_id)"
 						: "")
 				+ ")  AS tsk_events";
 	}
@@ -879,61 +976,62 @@ private static int booleanToInt(boolean value) {
 	private static boolean intToBoolean(int value) {
 		return value != 0;
 	}
-	
+
 	/**
-	 * Returns a list of TimelineEvents for the given filter and time range.
-	 * 
-	 * @param timeRange 
-	 * @param filter TimelineFilter.RootFilter for filtering data
-	 * 
-	 * @return	A list of TimelineEvents for given parameters, if filter is null 
-	 *			or times are invalid an empty list will be returned.
-	 * 
-	 * @throws TskCoreException 
+	 * Gets the timeline events that fall within a given time interval and
+	 * satisfy a given event filter.
+	 *
+	 * @param timeRange The time level.
+	 * @param filter    The event filter.
+	 *
+	 * @return	The list of events that fall within the specified interval and
+	 *         poass the specified filter.
+	 *
+	 * @throws TskCoreException If there is an error querying the case database.
 	 */
-	public List<TimelineEvent> getEvents(Interval timeRange, TimelineFilter.RootFilter filter) throws TskCoreException{
+	public List<TimelineEvent> getEvents(Interval timeRange, TimelineFilter.RootFilter filter) throws TskCoreException {
 		List<TimelineEvent> events = new ArrayList<>();
-		
+
 		Long startTime = timeRange.getStartMillis() / 1000;
 		Long endTime = timeRange.getEndMillis() / 1000;
 
 		if (Objects.equals(startTime, endTime)) {
 			endTime++; //make sure end is at least 1 millisecond after start
 		}
-		
+
 		if (filter == null) {
 			return events;
 		}
-		
+
 		if (endTime < startTime) {
 			return events;
 		}
 
 		//build dynamic parts of query
-        String querySql = "SELECT time, file_obj_id, data_source_obj_id, artifact_id, " // NON-NLS
-                          + "  event_id, " //NON-NLS
-                          + " hash_hit, " //NON-NLS
-                          + " tagged, " //NON-NLS
-                          + " event_type_id, super_type_id, "
-                          + " full_description, med_description, short_description " // NON-NLS
-                          + " FROM " + getAugmentedEventsTablesSQL(filter) // NON-NLS
-                          + " WHERE time >= " + startTime + " AND time < " + endTime + " AND " + getSQLWhere(filter) // NON-NLS
-                          + " ORDER BY time"; // NON-NLS
-		
-		sleuthkitCase.acquireSingleUserCaseReadLock();
-		try (CaseDbConnection con = sleuthkitCase.getConnection();
+		String querySql = "SELECT time, content_obj_id, data_source_obj_id, artifact_id, " // NON-NLS
+				+ "  event_id, " //NON-NLS
+				+ " hash_hit, " //NON-NLS
+				+ " tagged, " //NON-NLS
+				+ " event_type_id, super_type_id, "
+				+ " full_description, med_description, short_description " // NON-NLS
+				+ " FROM " + getAugmentedEventsTablesSQL(filter) // NON-NLS
+				+ " WHERE time >= " + startTime + " AND time < " + endTime + " AND " + getSQLWhere(filter) // NON-NLS
+				+ " ORDER BY time"; // NON-NLS
+
+		caseDB.acquireSingleUserCaseReadLock();
+		try (CaseDbConnection con = caseDB.getConnection();
 				Statement stmt = con.createStatement();
 				ResultSet resultSet = stmt.executeQuery(querySql);) {
-			
-			 while (resultSet.next()) {
-                int eventTypeID = resultSet.getInt("event_type_id");
+
+			while (resultSet.next()) {
+				int eventTypeID = resultSet.getInt("event_type_id");
 				TimelineEventType eventType = getEventType(eventTypeID).orElseThrow(()
 						-> new TskCoreException("Error mapping event type id " + eventTypeID + "to EventType."));//NON-NLS
 
-				TimelineEvent event =  new TimelineEvent(
+				TimelineEvent event = new TimelineEvent(
 						resultSet.getLong("event_id"), // NON-NLS
 						resultSet.getLong("data_source_obj_id"), // NON-NLS
-						resultSet.getLong("file_obj_id"), // NON-NLS
+						resultSet.getLong("content_obj_id"), // NON-NLS
 						resultSet.getLong("artifact_id"), // NON-NLS
 						resultSet.getLong("time"), // NON-NLS
 						eventType,
@@ -942,16 +1040,16 @@ public List<TimelineEvent> getEvents(Interval timeRange, TimelineFilter.RootFilt
 						resultSet.getString("short_description"), // NON-NLS
 						resultSet.getInt("hash_hit") != 0, //NON-NLS
 						resultSet.getInt("tagged") != 0);
-				
+
 				events.add(event);
-            }
-			
+			}
+
 		} catch (SQLException ex) {
 			throw new TskCoreException("Error getting events from db: " + querySql, ex); // NON-NLS
 		} finally {
-			sleuthkitCase.releaseSingleUserCaseReadLock();
+			caseDB.releaseSingleUserCaseReadLock();
 		}
-		
+
 		return events;
 	}
 
@@ -962,7 +1060,7 @@ public List<TimelineEvent> getEvents(Interval timeRange, TimelineFilter.RootFilt
 	 *
 	 * @return column name to use depending on if we want base types or subtypes
 	 */
-	static String typeColumnHelper(final boolean useSubTypes) {
+	private static String typeColumnHelper(final boolean useSubTypes) {
 		return useSubTypes ? "event_type_id" : "super_type_id"; //NON-NLS
 	}
 
@@ -986,26 +1084,14 @@ String getSQLWhere(TimelineFilter.RootFilter filter) {
 		return result;
 	}
 
-	String getDescriptionColumn(TimelineEvent.DescriptionLevel lod) {
-		switch (lod) {
-			case FULL:
-				return "full_description"; //NON-NLS
-			case MEDIUM:
-				return "med_description"; //NON-NLS
-			case SHORT:
-			default:
-				return "short_description"; //NON-NLS
-			}
-	}
-
-	String getTrueLiteral() {
-		switch (sleuthkitCase.getDatabaseType()) {
+	private String getTrueLiteral() {
+		switch (caseDB.getDatabaseType()) {
 			case POSTGRESQL:
 				return "TRUE";//NON-NLS
 			case SQLITE:
 				return "1";//NON-NLS
 			default:
-				throw new UnsupportedOperationException("Unsupported DB type: " + sleuthkitCase.getDatabaseType().name());//NON-NLS
+				throw new UnsupportedOperationException("Unsupported DB type: " + caseDB.getDatabaseType().name());//NON-NLS
 
 		}
 	}
@@ -1035,7 +1121,7 @@ public TimelineEvent getAddedEvent() {
 	 * @param <O> Output type.
 	 */
 	@FunctionalInterface
-	interface TSKCoreCheckedFunction<I, O> {
+	private interface TSKCoreCheckedFunction<I, O> {
 
 		O apply(I input) throws TskCoreException;
 	}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/CommunicationArtifactsHelper.java b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/CommunicationArtifactsHelper.java
index 31c2b5720930a7e35f6531e9d8d1109a054a805a..373fa2459369f9a337aa960af27ca63c78d8e820 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/CommunicationArtifactsHelper.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/CommunicationArtifactsHelper.java
@@ -159,23 +159,22 @@ public CommunicationArtifactsHelper(SleuthkitCase caseDb,
 	 * It creates an account instance with specified type & id, and uses it as
 	 * the self account.
 	 *
-	 * @param caseDb             Sleuthkit case db.
-	 * @param moduleName         Name of module using the helper.
-	 * @param srcFile            Source file being processed by the module.
-	 * @param accountsType		 Account type {@link Account.Type} created by
-	 *                           this module.
-	 * @param selfAccountType    Self account type to be created for this
-	 *                           module.
-	 * @param selfAccountAddress Account unique id for the self account.
+	 * @param caseDb          Sleuthkit case db.
+	 * @param moduleName      Name of module using the helper.
+	 * @param srcFile         Source file being processed by the module.
+	 * @param accountsType    Account type {@link Account.Type} created by this
+	 *                        module.
+	 * @param selfAccountType Self account type to be created for this module.
+	 * @param selfAccountId	  Account unique id for the self account.
 	 *
 	 * @throws TskCoreException	If there is an error creating the self account
 	 */
-	public CommunicationArtifactsHelper(SleuthkitCase caseDb, String moduleName, AbstractFile srcFile, Account.Type accountsType, Account.Type selfAccountType, Account.Address selfAccountAddress) throws TskCoreException {
+	public CommunicationArtifactsHelper(SleuthkitCase caseDb, String moduleName, AbstractFile srcFile, Account.Type accountsType, Account.Type selfAccountType, String selfAccountId) throws TskCoreException {
 
 		super(caseDb, moduleName, srcFile);
 
 		this.accountsType = accountsType;
-		this.selfAccountInstance = getSleuthkitCase().getCommunicationsManager().createAccountFileInstance(selfAccountType, selfAccountAddress.getUniqueID(), moduleName, getAbstractFile());
+		this.selfAccountInstance = getSleuthkitCase().getCommunicationsManager().createAccountFileInstance(selfAccountType, selfAccountId, moduleName, getAbstractFile());
 	}
 
 	/**
@@ -183,14 +182,15 @@ public CommunicationArtifactsHelper(SleuthkitCase caseDb, String moduleName, Abs
 	 * attributes. Also creates an account instance of specified type for the
 	 * contact with the specified ID.
 	 *
-	 * @param contactAccountUniqueID Unique id for contact account, required.
-	 * @param contactName            Name of contact, required.
-	 * @param phoneNumber            Primary phone number for contact, may be
-	 *                               empty or null.
-	 * @param homePhoneNumber        Home phone number, may be empty or null.
-	 * @param mobilePhoneNumber      Mobile phone number, may be empty or null.
-	 * @param emailAddr              Email address for the contact, may be empty
-	 *                               or null.
+	 * @param contactName       Contact name, required.
+	 * @param phoneNumber       Primary phone number for contact, may be empty
+	 *                          or null.
+	 * @param homePhoneNumber   Home phone number, may be empty or null.
+	 * @param mobilePhoneNumber Mobile phone number, may be empty or null.
+	 * @param emailAddr         Email address for the contact, may be empty or
+	 *                          null.
+	 *
+	 * At least one phone number or email address is required.
 	 *
 	 * @return Contact artifact created.
 	 *
@@ -198,10 +198,10 @@ public CommunicationArtifactsHelper(SleuthkitCase caseDb, String moduleName, Abs
 	 * @throws BlackboardException	If there is a problem posting the artifact.
 	 *
 	 */
-	public BlackboardArtifact addContact(String contactAccountUniqueID, String contactName,
+	public BlackboardArtifact addContact(String contactName,
 			String phoneNumber, String homePhoneNumber,
 			String mobilePhoneNumber, String emailAddr) throws TskCoreException, BlackboardException {
-		return addContact(contactAccountUniqueID, contactName, phoneNumber,
+		return addContact(contactName, phoneNumber,
 				homePhoneNumber, mobilePhoneNumber, emailAddr,
 				Collections.emptyList());
 	}
@@ -211,29 +211,57 @@ public BlackboardArtifact addContact(String contactAccountUniqueID, String conta
 	 * attributes. Also creates an account instance for the contact with the
 	 * specified ID.
 	 *
-	 * @param contactAccountUniqueID Unique id for contact account, required.
-	 * @param contactName            Name of contact, required.
-	 * @param phoneNumber            Primary phone number for contact, may be
-	 *                               empty or null.
-	 * @param homePhoneNumber        Home phone number, may be empty or null.
-	 * @param mobilePhoneNumber      Mobile phone number, may be empty or null.
-	 * @param emailAddr              Email address for the contact, may be empty
-	 *                               or null.
+	 * @param contactName          Contact name, required
+	 * @param phoneNumber          Primary phone number for contact, may be
+	 *                             empty or null.
+	 * @param homePhoneNumber      Home phone number, may be empty or null.
+	 * @param mobilePhoneNumber    Mobile phone number, may be empty or null.
+	 * @param emailAddr            Email address for the contact, may be empty
+	 *                             or null.
+	 *
+	 * At least one phone number or email address or an Id is required.
+	 * An Id may be passed in as a TSK_ID attribute in additionalAttributes.
 	 *
-	 * @param additionalAttributes   Additional attributes for contact, may be
-	 *                               an empty list.
+	 * @param additionalAttributes Additional attributes for contact, may be an
+	 *                             empty list.
 	 *
 	 * @return contact artifact created.
 	 *
-	 * @throws TskCoreException		If there is an error creating the artifact.
+	 * @throws TskCoreException		  If there is an error creating the artifact.
 	 * @throws BlackboardException	If there is a problem posting the artifact.
 	 *
 	 */
-	public BlackboardArtifact addContact(String contactAccountUniqueID, String contactName,
+	public BlackboardArtifact addContact(String contactName,
 			String phoneNumber, String homePhoneNumber,
 			String mobilePhoneNumber, String emailAddr,
 			Collection<BlackboardAttribute> additionalAttributes) throws TskCoreException, BlackboardException {
 
+		// Contact name must be provided
+		if (StringUtils.isEmpty(contactName)) {
+			throw new IllegalArgumentException("Contact name must be specified.");
+		}
+
+		// check if the caller has included any phone/email/id in addtional attributes
+		boolean hasAnyIdAttribute = false;
+		if (additionalAttributes != null) {
+			for (BlackboardAttribute attr : additionalAttributes) {
+				if ((attr.getAttributeType().getTypeName().startsWith("TSK_PHONE")) ||
+					(attr.getAttributeType().getTypeName().startsWith("TSK_EMAIL"))	||
+					(attr.getAttributeType().getTypeName().startsWith("TSK_ID")))  {
+						hasAnyIdAttribute = true;
+						break;
+				}
+			}
+		}
+
+		// At least one phone number or email address 
+		// or an optional attribute with phone/email/id must be provided
+		if (StringUtils.isEmpty(phoneNumber) && StringUtils.isEmpty(homePhoneNumber)
+				&& StringUtils.isEmpty(mobilePhoneNumber) && StringUtils.isEmpty(emailAddr)
+				&& (!hasAnyIdAttribute)) {
+			throw new IllegalArgumentException("At least one phone number or email address or an id must be provided.");
+		}
+
 		BlackboardArtifact contactArtifact;
 		Collection<BlackboardAttribute> attributes = new ArrayList<>();
 
@@ -252,17 +280,58 @@ public BlackboardArtifact addContact(String contactAccountUniqueID, String conta
 		attributes.addAll(additionalAttributes);
 		contactArtifact.addAttributes(attributes);
 
-		// Find/Create an account instance for the contact
-		// Create a relationship between selfAccount and contactAccount
-		AccountFileInstance contactAccountInstance = createAccountInstance(accountsType, contactAccountUniqueID);
-		addRelationship(selfAccountInstance, contactAccountInstance, contactArtifact, Relationship.Type.CONTACT, 0);
-
+		// create an account for each specified contact method, and a relationship with self account
+		createContactMethodAccountAndRelationship(Account.Type.PHONE, phoneNumber, contactArtifact, 0);
+		createContactMethodAccountAndRelationship(Account.Type.PHONE, homePhoneNumber, contactArtifact, 0);
+		createContactMethodAccountAndRelationship(Account.Type.PHONE, mobilePhoneNumber, contactArtifact, 0);
+		createContactMethodAccountAndRelationship(Account.Type.EMAIL, emailAddr, contactArtifact, 0);
+
+		// if the additional attribute list has any phone/email/id attributes, create accounts & relationships for those. 
+		if ((additionalAttributes != null) && hasAnyIdAttribute) {
+			for (BlackboardAttribute bba : additionalAttributes) {
+                if (bba.getAttributeType().getTypeName().startsWith("TSK_PHONE")) {
+					createContactMethodAccountAndRelationship(Account.Type.PHONE, bba.getValueString(), contactArtifact, 0);
+                } else if (bba.getAttributeType().getTypeName().startsWith("TSK_EMAIL")) {
+                    createContactMethodAccountAndRelationship(Account.Type.EMAIL, bba.getValueString(), contactArtifact, 0);
+                } else if (bba.getAttributeType().getTypeName().startsWith("TSK_ID")) {
+                    createContactMethodAccountAndRelationship(this.accountsType, bba.getValueString(), contactArtifact, 0);
+                } 
+            }
+		}
+		
 		// post artifact 
 		getSleuthkitCase().getBlackboard().postArtifact(contactArtifact, getModuleName());
 
 		return contactArtifact;
 	}
 
+	/**
+	 * Creates a contact's account instance of specified account type, if the
+	 * account id is not null/empty.
+	 *
+	 * Also creates a CONTACT relationship between the self account and the new
+	 * contact account.
+	 */
+	private void createContactMethodAccountAndRelationship(Account.Type accountType,
+			String accountUniqueID, BlackboardArtifact sourceArtifact,
+			long dateTime) throws TskCoreException {
+
+		// Find/Create an account instance for each of the contact method
+		// Create a relationship between selfAccount and contactAccount
+		if (!StringUtils.isEmpty(accountUniqueID)) {
+			AccountFileInstance contactAccountInstance = createAccountInstance(accountsType, accountUniqueID);
+
+			// Create a relationship between self account and the contact account
+			try {
+				getSleuthkitCase().getCommunicationsManager().addRelationships(selfAccountInstance,
+						Collections.singletonList(contactAccountInstance), sourceArtifact, Relationship.Type.CONTACT, dateTime);
+			} catch (TskDataException ex) {
+				throw new TskCoreException(String.format("Failed to create relationship between account = %s and account = %s.",
+						selfAccountInstance.getAccount(), contactAccountInstance.getAccount()), ex);
+			}
+		}
+	}
+
 	/**
 	 * Creates an account file instance{@link AccountFileInstance} associated
 	 * with the DB file.
@@ -279,31 +348,6 @@ private AccountFileInstance createAccountInstance(Account.Type accountType, Stri
 		return getSleuthkitCase().getCommunicationsManager().createAccountFileInstance(accountType, accountUniqueID, getModuleName(), getAbstractFile());
 	}
 
-	/**
-	 * Adds a relations between the two specified account instances.
-	 *
-	 * @param selfAccount      'Self' account.
-	 * @param otherAccount     Other account.
-	 * @param sourceArtifact   Artifact from which the relationship is derived.
-	 * @param relationshipType Type of relationship.
-	 * @param dateTime         Date/time of relationship.
-	 *
-	 * @throws TskCoreException If there is an error creating relationship.
-	 */
-	private void addRelationship(AccountFileInstance selfAccountInstance, AccountFileInstance otherAccountInstance,
-			BlackboardArtifact sourceArtifact, Relationship.Type relationshipType, long dateTime) throws TskCoreException {
-
-		if (selfAccountInstance.getAccount() != otherAccountInstance.getAccount()) {
-			try {
-				getSleuthkitCase().getCommunicationsManager().addRelationships(selfAccountInstance,
-						Collections.singletonList(otherAccountInstance), sourceArtifact, relationshipType, dateTime);
-			} catch (TskDataException ex) {
-				throw new TskCoreException(String.format("Failed to create relationship between account = %s and account = %s.",
-						selfAccountInstance.getAccount(), otherAccountInstance.getAccount()), ex);
-			}
-		}
-	}
-
 	/**
 	 * Adds a TSK_MESSAGE artifact.
 	 *
@@ -312,13 +356,13 @@ private void addRelationship(AccountFileInstance selfAccountInstance, AccountFil
 	 *
 	 * @param messageType Message type, required.
 	 * @param direction   Message direction, UNKNOWN if not available.
-	 * @param fromAddress Sender address, may be null.
-	 * @param toAddress	  Recipient address, may be null.
+	 * @param senderId    Sender address id, may be null.
+	 * @param recipientId Recipient id, may be null.
 	 * @param dateTime    Date/time of message, 0 if not available.
 	 * @param readStatus  Message read status, UNKNOWN if not available.
 	 * @param subject     Message subject, may be empty or null.
 	 * @param messageText Message body, may be empty or null.
-	 * @param threadId,   Message thread id, may be empty or null.
+	 * @param threadId    Message thread id, may be empty or null.
 	 *
 	 * @return Message artifact.
 	 *
@@ -328,12 +372,12 @@ private void addRelationship(AccountFileInstance selfAccountInstance, AccountFil
 	public BlackboardArtifact addMessage(
 			String messageType,
 			CommunicationDirection direction,
-			Account.Address fromAddress,
-			Account.Address toAddress,
+			String senderId,
+			String recipientId,
 			long dateTime, MessageReadStatus readStatus,
 			String subject, String messageText, String threadId) throws TskCoreException, BlackboardException {
 		return addMessage(messageType, direction,
-				fromAddress, toAddress, dateTime, readStatus,
+				senderId, recipientId, dateTime, readStatus,
 				subject, messageText, threadId,
 				Collections.emptyList());
 	}
@@ -346,13 +390,13 @@ public BlackboardArtifact addMessage(
 	 *
 	 * @param messageType         Message type, required.
 	 * @param direction           Message direction, UNKNOWN if not available.
-	 * @param fromAddress         Sender address, may be null.
-	 * @param toAddress	          Recipient address, may be null.
+	 * @param senderId            Sender id, may be null.
+	 * @param recipientId         Recipient id, may be null.
 	 * @param dateTime            Date/time of message, 0 if not available.
 	 * @param readStatus          Message read status, UNKNOWN if not available.
 	 * @param subject             Message subject, may be empty or null.
 	 * @param messageText         Message body, may be empty or null.
-	 * @param threadId,           Message thread id, may be empty or null.
+	 * @param threadId            Message thread id, may be empty or null.
 	 * @param otherAttributesList Additional attributes, may be an empty list.
 	 *
 	 * @return Message artifact.
@@ -362,15 +406,15 @@ public BlackboardArtifact addMessage(
 	 */
 	public BlackboardArtifact addMessage(String messageType,
 			CommunicationDirection direction,
-			Account.Address fromAddress,
-			Account.Address toAddress,
+			String senderId,
+			String recipientId,
 			long dateTime, MessageReadStatus readStatus, String subject,
 			String messageText, String threadId,
 			Collection<BlackboardAttribute> otherAttributesList) throws TskCoreException, BlackboardException {
 
 		return addMessage(messageType, direction,
-				fromAddress,
-				Arrays.asList(toAddress),
+				senderId,
+				Arrays.asList(recipientId),
 				dateTime, readStatus,
 				subject, messageText, threadId,
 				otherAttributesList);
@@ -383,16 +427,15 @@ public BlackboardArtifact addMessage(String messageType,
 	 * relationship between the self account and the sender/receiver accounts.
 	 *
 	 *
-	 * @param messageType    Message type, required.
-	 * @param direction      Message direction, UNKNOWN if not available.
-	 * @param fromAddress    Sender address, may be null.
-	 * @param recipientsList Recipient address list, may be null or empty an
-	 *                       list.
-	 * @param dateTime       Date/time of message, 0 if not available.
-	 * @param readStatus     Message read status, UNKNOWN if not available.
-	 * @param subject        Message subject, may be empty or null.
-	 * @param messageText    Message body, may be empty or null.
-	 * @param threadId,      Message thread id, may be empty or null.
+	 * @param messageType      Message type, required.
+	 * @param direction        Message direction, UNKNOWN if not available.
+	 * @param senderId         Sender id, may be null.
+	 * @param recipientIdsList Recipient ids list, may be null or empty list.
+	 * @param dateTime         Date/time of message, 0 if not available.
+	 * @param readStatus       Message read status, UNKNOWN if not available.
+	 * @param subject          Message subject, may be empty or null.
+	 * @param messageText      Message body, may be empty or null.
+	 * @param threadId         Message thread id, may be empty or null.
 	 *
 	 * @return Message artifact.
 	 *
@@ -401,12 +444,12 @@ public BlackboardArtifact addMessage(String messageType,
 	 */
 	public BlackboardArtifact addMessage(String messageType,
 			CommunicationDirection direction,
-			Account.Address fromAddress,
-			List<Account.Address> recipientsList,
+			String senderId,
+			List<String> recipientIdsList,
 			long dateTime, MessageReadStatus readStatus,
 			String subject, String messageText, String threadId) throws TskCoreException, BlackboardException {
 		return addMessage(messageType, direction,
-				fromAddress, recipientsList,
+				senderId, recipientIdsList,
 				dateTime, readStatus,
 				subject, messageText, threadId,
 				Collections.emptyList());
@@ -415,19 +458,18 @@ public BlackboardArtifact addMessage(String messageType,
 	/**
 	 * Adds a TSK_MESSAGE artifact.
 	 *
-	 * Also creates an account instance for the sender/receivers, and creates a
-	 * relationship between the self account and the sender/receivers account.
+	 * Also creates accounts for the sender/receivers, and creates relationships
+	 * between the sender/receivers account.
 	 *
 	 * @param messageType         Message type, required.
 	 * @param direction           Message direction, UNKNOWN if not available.
-	 * @param fromAddress         Sender address, may be null.
-	 * @param recipientsList      Recipient address list, may be null or empty
-	 *                            an list.
+	 * @param senderId            Sender id, may be null.
+	 * @param recipientIdsList    Recipient list, may be null or empty an list.
 	 * @param dateTime            Date/time of message, 0 if not available.
 	 * @param readStatus          Message read status, UNKNOWN if not available.
 	 * @param subject             Message subject, may be empty or null.
 	 * @param messageText         Message body, may be empty or null.
-	 * @param threadId,           Message thread id, may be empty or null.
+	 * @param threadId            Message thread id, may be empty or null.
 	 * @param otherAttributesList Other attributes, may be an empty list.
 	 *
 	 * @return Message artifact.
@@ -437,8 +479,8 @@ public BlackboardArtifact addMessage(String messageType,
 	 */
 	public BlackboardArtifact addMessage(String messageType,
 			CommunicationDirection direction,
-			Account.Address fromAddress,
-			List<Account.Address> recipientsList,
+			String senderId,
+			List<String> recipientIdsList,
 			long dateTime, MessageReadStatus readStatus,
 			String subject, String messageText,
 			String threadId,
@@ -458,12 +500,29 @@ public BlackboardArtifact addMessage(String messageType,
 		addMessageReadStatusIfKnown(readStatus, attributes);
 		addCommDirectionIfKnown(direction, attributes);
 
-		if (fromAddress != null && !StringUtils.isEmpty(fromAddress.getDisplayName())) {
-			attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_FROM, getModuleName(), fromAddress.getDisplayName()));
+		// set sender attribute and create sender account
+		AccountFileInstance senderAccountInstance;
+		if (StringUtils.isEmpty(senderId)) {
+			senderAccountInstance = selfAccountInstance;
+			addAttributeIfNotNull(ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_FROM, selfAccountInstance.getAccount().getTypeSpecificID(), attributes);
+		} else {
+			senderAccountInstance = createAccountInstance(accountsType, senderId);
+			addAttributeIfNotNull(ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_FROM, senderId, attributes);
+		}
+
+		// set recipient attribute and create recipient accounts
+		List<AccountFileInstance> recipientAccountsList = new ArrayList();
+		String recipientsStr = "";
+		if (recipientIdsList != null) {
+			for (String recipient : recipientIdsList) {
+				if (!StringUtils.isEmpty(recipient)) {
+					recipientAccountsList.add(createAccountInstance(accountsType, recipient));
+				}
+			}
+			// Create a comma separated string of recipients
+			recipientsStr = addressListToString(recipientIdsList);
+			addAttributeIfNotNull(ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_TO, recipientsStr, attributes);
 		}
-		// Create a comma separated string of recipients
-		String toAddresses = addressListToString(recipientsList);
-		addAttributeIfNotNull(ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_TO, toAddresses, attributes);
 
 		addAttributeIfNotNull(ATTRIBUTE_TYPE.TSK_SUBJECT, subject, attributes);
 		addAttributeIfNotNull(ATTRIBUTE_TYPE.TSK_TEXT, messageText, attributes);
@@ -473,11 +532,14 @@ public BlackboardArtifact addMessage(String messageType,
 		attributes.addAll(otherAttributesList);
 		msgArtifact.addAttributes(attributes);
 
-		// create account and relationship with sender
-		createSenderAccountAndRelationship(fromAddress, msgArtifact, Relationship.Type.MESSAGE, dateTime);
-
-		// create account and relationship with each recipient  
-		createRecipientAccountsAndRelationships(recipientsList, msgArtifact, Relationship.Type.MESSAGE, dateTime);
+		// create sender/recipient relationships  
+		try {
+			getSleuthkitCase().getCommunicationsManager().addRelationships(senderAccountInstance,
+					recipientAccountsList, msgArtifact, Relationship.Type.MESSAGE, dateTime);
+		} catch (TskDataException ex) {
+			throw new TskCoreException(String.format("Failed to create Message relationships between sender account = %s and recipients = %s.",
+					senderAccountInstance.getAccount().getTypeSpecificID(), recipientsStr), ex);
+		}
 
 		// post artifact 
 		getSleuthkitCase().getBlackboard().postArtifact(msgArtifact, getModuleName());
@@ -494,8 +556,11 @@ public BlackboardArtifact addMessage(String messageType,
 	 * between the self account and the callee account.
 	 *
 	 * @param direction     Call direction, UNKNOWN if not available.
-	 * @param fromAddress   Caller address, may be null.
-	 * @param toAddress			  Callee address, may be null.
+	 * @param callerId      Caller id, may be null.
+	 * @param calleeId      Callee id, may be null.
+	 *
+	 * At least one of the two must be provided - the caller Id, or a callee id.
+	 *
 	 * @param startDateTime Start date/time, 0 if not available.
 	 * @param endDateTime   End date/time, 0 if not available.
 	 * @param mediaType     Media type.
@@ -506,9 +571,9 @@ public BlackboardArtifact addMessage(String messageType,
 	 * @throws BlackboardException If there is a problem posting the artifact.
 	 */
 	public BlackboardArtifact addCalllog(CommunicationDirection direction,
-			Account.Address fromAddress, Account.Address toAddress,
+			String callerId, String calleeId,
 			long startDateTime, long endDateTime, CallMediaType mediaType) throws TskCoreException, BlackboardException {
-		return addCalllog(direction, fromAddress, toAddress,
+		return addCalllog(direction, callerId, calleeId,
 				startDateTime, endDateTime, mediaType,
 				Collections.emptyList());
 	}
@@ -521,8 +586,11 @@ public BlackboardArtifact addCalllog(CommunicationDirection direction,
 	 * between the self account and the callee account.
 	 *
 	 * @param direction           Call direction, UNKNOWN if not available.
-	 * @param fromAddress         Caller address, may be null.
-	 * @param toAddress			        Callee address, may be null.
+	 * @param callerId            Caller id, may be null.
+	 * @param calleeId            Callee id, may be null.
+	 *
+	 * At least one of the two must be provided - the caller Id, or a callee id.
+	 *
 	 * @param startDateTime       Start date/time, 0 if not available.
 	 * @param endDateTime         End date/time, 0 if not available.
 	 * @param mediaType           Media type.
@@ -534,14 +602,14 @@ public BlackboardArtifact addCalllog(CommunicationDirection direction,
 	 * @throws BlackboardException If there is a problem posting the artifact.
 	 */
 	public BlackboardArtifact addCalllog(CommunicationDirection direction,
-			Account.Address fromAddress,
-			Account.Address toAddress,
+			String callerId,
+			String calleeId,
 			long startDateTime, long endDateTime,
 			CallMediaType mediaType,
 			Collection<BlackboardAttribute> otherAttributesList) throws TskCoreException, BlackboardException {
 		return addCalllog(direction,
-				fromAddress,
-				Arrays.asList(toAddress),
+				callerId,
+				Arrays.asList(calleeId),
 				startDateTime, endDateTime,
 				mediaType,
 				otherAttributesList);
@@ -555,8 +623,11 @@ public BlackboardArtifact addCalllog(CommunicationDirection direction,
 	 * between the self account and each callee account.
 	 *
 	 * @param direction     Call direction, UNKNOWN if not available.
-	 * @param fromAddress   Caller address, may be null.
-	 * @param toAddressList callee address list, may be an empty list.
+	 * @param callerId      Caller id, may be null.
+	 * @param calleeIdsList Callee list, may be an empty list.
+	 *
+	 * At least one of the two must be provided - the caller Id, or a callee id.
+	 *
 	 * @param startDateTime Start date/time, 0 if not available.
 	 * @param endDateTime   End date/time, 0 if not available.
 	 * @param mediaType     Call media type, UNKNOWN if not available.
@@ -567,12 +638,12 @@ public BlackboardArtifact addCalllog(CommunicationDirection direction,
 	 * @throws BlackboardException If there is a problem posting the artifact.
 	 */
 	public BlackboardArtifact addCalllog(CommunicationDirection direction,
-			Account.Address fromAddress,
-			Collection<Account.Address> toAddressList,
+			String callerId,
+			Collection<String> calleeIdsList,
 			long startDateTime, long endDateTime,
 			CallMediaType mediaType) throws TskCoreException, BlackboardException {
 
-		return addCalllog(direction, fromAddress, toAddressList,
+		return addCalllog(direction, callerId, calleeIdsList,
 				startDateTime, endDateTime,
 				mediaType,
 				Collections.emptyList());
@@ -581,13 +652,16 @@ public BlackboardArtifact addCalllog(CommunicationDirection direction,
 	/**
 	 * Adds a TSK_CALLLOG artifact.
 	 *
-	 * Also creates an account instance for the caller/callees, and creates a
-	 * relationship between the self account and the caller account as well
-	 * between the self account and each callee account.
+	 * Also creates an account instance for the caller and each of the callees,
+	 * and creates relationships between caller and callees.
 	 *
 	 * @param direction           Call direction, UNKNOWN if not available.
-	 * @param fromAddress         Caller address, may be null.
-	 * @param toAddressList       callee address list, may be an empty list.
+	 * @param callerId            Caller id, required for incoming call.
+	 * @param calleeIdsList       Callee ids list, required for an outgoing
+	 *                            call.
+	 *
+	 * At least one of the two must be provided - the caller Id, or a callee id.
+	 *
 	 * @param startDateTime       Start date/time, 0 if not available.
 	 * @param endDateTime         End date/time, 0 if not available.
 	 * @param mediaType           Call media type, UNKNOWN if not available.
@@ -599,11 +673,17 @@ public BlackboardArtifact addCalllog(CommunicationDirection direction,
 	 * @throws BlackboardException If there is a problem posting the artifact.
 	 */
 	public BlackboardArtifact addCalllog(CommunicationDirection direction,
-			Account.Address fromAddress,
-			Collection<Account.Address> toAddressList,
+			String callerId,
+			Collection<String> calleeIdsList,
 			long startDateTime, long endDateTime,
 			CallMediaType mediaType,
 			Collection<BlackboardAttribute> otherAttributesList) throws TskCoreException, BlackboardException {
+
+		// Either caller id or a callee id must be provided.
+		if (StringUtils.isEmpty(callerId) && (isEffectivelyEmpty(calleeIdsList))) {
+			throw new IllegalArgumentException("Either a caller id, or at least one callee id must be provided for a call log.");
+		}
+
 		BlackboardArtifact callLogArtifact;
 		Collection<BlackboardAttribute> attributes = new ArrayList<>();
 
@@ -615,24 +695,55 @@ public BlackboardArtifact addCalllog(CommunicationDirection direction,
 		addAttributeIfNotZero(ATTRIBUTE_TYPE.TSK_DATETIME_END, endDateTime, attributes);
 		addCommDirectionIfKnown(direction, attributes);
 
-		if (fromAddress != null) {
-			addAttributeIfNotNull(ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_FROM, fromAddress.getUniqueID(), attributes);
-			addAttributeIfNotNull(ATTRIBUTE_TYPE.TSK_NAME, fromAddress.getDisplayName(), attributes);
+		// set FROM attribute and create a caller account
+		AccountFileInstance callerAccountInstance;
+		if (StringUtils.isEmpty(callerId)) {
+			// for an Outgoing call, if no caller is specified, assume self account is the caller
+			if (direction == CommunicationDirection.OUTGOING) {
+				callerAccountInstance = selfAccountInstance;
+				addAttributeIfNotNull(ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_FROM, selfAccountInstance.getAccount().getTypeSpecificID(), attributes);
+			} else { // incoming call without a caller id
+				throw new IllegalArgumentException("Caller Id not provided for incoming call.");
+			}
+		} else {
+			callerAccountInstance = createAccountInstance(accountsType, callerId);
+			addAttributeIfNotNull(ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_FROM, callerId, attributes);
 		}
 
-		// Create a comma separated string of recipients
-		String toAddresses = addressListToString(toAddressList);
-		addAttributeIfNotNull(ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_TO, toAddresses, attributes);
+		// Create a comma separated string of callee
+		List<AccountFileInstance> recipientAccountsList = new ArrayList();
+		String calleesStr = "";
+		if (! isEffectivelyEmpty(calleeIdsList)) {
+			calleesStr = addressListToString(calleeIdsList);
+			addAttributeIfNotNull(ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_TO, calleesStr, attributes);
+
+			for (String callee : calleeIdsList) {
+				if (!StringUtils.isEmpty(callee)) {
+					recipientAccountsList.add(createAccountInstance(accountsType, callee));
+				}
+			}
+		} else {
+			// For incoming call, if no callee specified, assume self account is callee
+			if (direction == CommunicationDirection.INCOMING) {
+				addAttributeIfNotNull(ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_TO, this.selfAccountInstance.getAccount().getTypeSpecificID(), attributes);
+				recipientAccountsList.add(this.selfAccountInstance);
+			} else { // outgoing call without any callee
+				throw new IllegalArgumentException("Callee not provided for an outgoing call.");
+			}
+		}
 
 		// add attributes to artifact
 		attributes.addAll(otherAttributesList);
 		callLogArtifact.addAttributes(attributes);
 
-		// Create a relationship between selfAccount and caller
-		createSenderAccountAndRelationship(fromAddress, callLogArtifact, Relationship.Type.CALL_LOG, startDateTime);
-
-		// Create a relationship between selfAccount and each callee
-		createRecipientAccountsAndRelationships(toAddressList, callLogArtifact, Relationship.Type.CALL_LOG, startDateTime);
+		// create relationships between caller/callees
+		try {
+			getSleuthkitCase().getCommunicationsManager().addRelationships(callerAccountInstance,
+					recipientAccountsList, callLogArtifact, Relationship.Type.CALL_LOG, startDateTime);
+		} catch (TskDataException ex) {
+			throw new TskCoreException(String.format("Failed to create Call log relationships between caller account = %s and callees = %s.",
+					callerAccountInstance.getAccount(), calleesStr), ex);
+		}
 
 		// post artifact 
 		getSleuthkitCase().getBlackboard().postArtifact(callLogArtifact, getModuleName());
@@ -642,17 +753,17 @@ public BlackboardArtifact addCalllog(CommunicationDirection direction,
 	}
 
 	/**
-	 * Converts a list of addresses into a single comma separated string of
-	 * addresses.
+	 * Converts a list of ids into a single comma separated string.
 	 */
-	private String addressListToString(Collection<Account.Address> addressList) {
+	private String addressListToString(Collection<String> addressList) {
 
 		String toAddresses = "";
 		if (addressList != null && (!addressList.isEmpty())) {
 			StringBuilder toAddressesSb = new StringBuilder();
-			for (Account.Address address : addressList) {
-				String displayAddress = !StringUtils.isEmpty(address.getDisplayName()) ? address.getDisplayName() : address.getUniqueID();
-				toAddressesSb = toAddressesSb.length() > 0 ? toAddressesSb.append(",").append(displayAddress) : toAddressesSb.append(displayAddress);
+			for (String address : addressList) {
+				if (!StringUtils.isEmpty(address)) {
+					toAddressesSb = toAddressesSb.length() > 0 ? toAddressesSb.append(", ").append(address) : toAddressesSb.append(address);
+				}
 			}
 			toAddresses = toAddressesSb.toString();
 		}
@@ -660,6 +771,29 @@ private String addressListToString(Collection<Account.Address> addressList) {
 		return toAddresses;
 	}
 
+	/**
+	 * Checks if the given list of ids has at least one non-null non-blank id.
+	 *
+	 * @param addressList List of string ids.
+	 *
+	 * @return false if the list has at least one non-null non-blank id,
+	 *         otherwise true.
+	 *
+	 */
+	private boolean isEffectivelyEmpty(Collection<String> idList) {
+
+		if (idList == null || idList.isEmpty()) {
+			return true;
+		}
+		
+		for (String id: idList) {
+			if (!StringUtils.isEmpty(id))
+				return false;
+		}
+		
+		return true;
+				
+	}
 	/**
 	 * Adds communication direction attribute to the list, if it is not unknown.
 	 */
@@ -678,33 +812,4 @@ private void addMessageReadStatusIfKnown(MessageReadStatus readStatus, Collectio
 		}
 	}
 
-	/**
-	 * Creates an account & relationship for sender, if the sender address is
-	 * not null/empty.
-	 */
-	private void createSenderAccountAndRelationship(Account.Address fromAddress,
-			BlackboardArtifact artifact, Relationship.Type relationshipType, long dateTime) throws TskCoreException {
-		if (fromAddress != null) {
-			AccountFileInstance senderAccountInstance = createAccountInstance(accountsType, fromAddress.getUniqueID());
-
-			// Create a relationship between selfAccount and sender account
-			addRelationship(selfAccountInstance, senderAccountInstance, artifact, relationshipType, dateTime);
-		}
-	}
-
-	/**
-	 * Creates accounts & relationship with each recipient, if the recipient
-	 * list is not null/empty.
-	 */
-	private void createRecipientAccountsAndRelationships(Collection<Account.Address> toAddressList,
-			BlackboardArtifact artifact, Relationship.Type relationshipType, long dateTime) throws TskCoreException {
-		// Create a relationship between selfAccount and each recipient
-		if (toAddressList != null) {
-			for (Account.Address recipient : toAddressList) {
-				AccountFileInstance calleeAccountInstance = createAccountInstance(accountsType, recipient.getUniqueID());
-				addRelationship(selfAccountInstance, calleeAccountInstance, artifact, relationshipType, (dateTime > 0) ? dateTime : 0);
-			}
-		}
-	}
-
 }
diff --git a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/WebBrowserArtifactsHelper.java b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/WebBrowserArtifactsHelper.java
index b472fd4efa9afcba5993d0d5bf36eab5fedc678b..5eb743af6a792a3d54e4879a725512ecc4ee060b 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/WebBrowserArtifactsHelper.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/blackboardutils/WebBrowserArtifactsHelper.java
@@ -189,9 +189,9 @@ public BlackboardArtifact addWebCookie(String url,
 	/**
 	 * Adds a TSK_WEB_DOWNNLOAD artifact.
 	 *
-	 * @param path        Path of downloaded file, required.
-	 * @param startTime   Date/time downloaded, 0 if not available.
 	 * @param url         URL downloaded from, required.
+	 * @param startTime   Date/time downloaded, 0 if not available.
+	 * @param path        Path of downloaded file, required.
 	 * @param programName Program that initiated the download, may be empty or
 	 *                    null.
 	 *
@@ -200,16 +200,16 @@ public BlackboardArtifact addWebCookie(String url,
 	 * @throws TskCoreException    If there is an error creating the artifact.
 	 * @throws BlackboardException If there is a problem posting the artifact.
 	 */
-	public BlackboardArtifact addWebDownload(String path, long startTime, String url, String programName) throws TskCoreException, BlackboardException {
+	public BlackboardArtifact addWebDownload(String url, long startTime, String path, String programName) throws TskCoreException, BlackboardException {
 		return addWebDownload(path, startTime, url, programName, Collections.emptyList());
 	}
 
 	/**
 	 * Adds a TSK_WEB_DOWNNLOAD artifact.
 	 *
-	 * @param path                Path of downloaded file, required.
-	 * @param startTime           Date/time downloaded, 0 if not available.
 	 * @param url                 URL downloaded from, required.
+	 * @param startTime           Date/time downloaded, 0 if not available.
+	 * @param path                Path of downloaded file, required.
 	 * @param programName         Program that initiated the download, may be
 	 *                            empty or null.
 	 * @param otherAttributesList Other attributes, may be an empty list.
@@ -219,7 +219,7 @@ public BlackboardArtifact addWebDownload(String path, long startTime, String url
 	 * @throws TskCoreException	   If there is an error creating the artifact.
 	 * @throws BlackboardException If there is a problem posting the artifact.
 	 */
-	public BlackboardArtifact addWebDownload(String path, long startTime, String url, String programName,
+	public BlackboardArtifact addWebDownload(String url, long startTime, String path, String programName,
 			Collection<BlackboardAttribute> otherAttributesList) throws TskCoreException, BlackboardException {
 
 		BlackboardArtifact webDownloadArtifact;
@@ -393,7 +393,7 @@ public BlackboardArtifact addWebFormAutofill(String name, String value,
 	 * @param accessTime   Last access time, may be 0 if not available.
 	 * @param referrer     Referrer, may be empty or null.
 	 * @param title        Website title, may be empty or null.
-	 * @param programName, Application/program recording the history, may be
+	 * @param programName  Application/program recording the history, may be
 	 *                     empty or null.
 	 *
 	 * @return Web history artifact created.
@@ -414,7 +414,7 @@ public BlackboardArtifact addWebHistory(String url, long accessTime,
 	 * @param accessTime          Last access time, may be 0 if not available.
 	 * @param referrer            Referrer, may be empty or null.
 	 * @param title               Website title, may be empty or null.
-	 * @param programName,        Application/program recording the history, may
+	 * @param programName         Application/program recording the history, may
 	 *                            be empty or null.
 	 * @param otherAttributesList Other attributes, may be an empty list.
 	 *
diff --git a/bindings/java/test/org/sleuthkit/datamodel/timeline/EventTypeFilterTest.java b/bindings/java/test/org/sleuthkit/datamodel/timeline/EventTypeFilterTest.java
index a581c2ee29b77ac84cdf74b37ef136eacb68b93f..c4b700ba5516a3fcb65ad8094d6d8ed99b307cce 100644
--- a/bindings/java/test/org/sleuthkit/datamodel/timeline/EventTypeFilterTest.java
+++ b/bindings/java/test/org/sleuthkit/datamodel/timeline/EventTypeFilterTest.java
@@ -35,11 +35,11 @@ public class EventTypeFilterTest {
 	public void testGetEventType() {
 		System.out.println("getEventType");
 		EventTypeFilter instance = new EventTypeFilter(TimelineEventType.ROOT_EVENT_TYPE);
-		assertEquals(TimelineEventType.ROOT_EVENT_TYPE, instance.getEventType());
+		assertEquals(TimelineEventType.ROOT_EVENT_TYPE, instance.getRootEventType());
 		instance = new EventTypeFilter(TimelineEventType.FILE_SYSTEM);
-		assertEquals(TimelineEventType.FILE_SYSTEM, instance.getEventType());
+		assertEquals(TimelineEventType.FILE_SYSTEM, instance.getRootEventType());
 		instance = new EventTypeFilter(TimelineEventType.MESSAGE);
-		assertEquals(TimelineEventType.MESSAGE, instance.getEventType());
+		assertEquals(TimelineEventType.MESSAGE, instance.getRootEventType());
 	}
 
 	/**
diff --git a/configure.ac b/configure.ac
index 2a70534131ce9bd48578fb7b6f1b47d71061c0c8..c89e0029076cf889eb8ad398c4ff398cdd6de9bd 100644
--- a/configure.ac
+++ b/configure.ac
@@ -4,7 +4,7 @@ dnl Process this file with autoconf to produce a configure script.
 
 AC_PREREQ(2.59)
 
-AC_INIT(sleuthkit, 4.6.7)
+AC_INIT(sleuthkit, 4.7.0)
 m4_include([m4/ax_pthread.m4])
 dnl include the version from 1.12.1. This will work for
 m4_include([m4/cppunit.m4])
diff --git a/debian/changelog b/debian/changelog
index 3802a67b7411953aadf3a86f8516a0dd2b82ba81..0d09c5daf67f69edc04998773f8b1a927fd042e3 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,4 +1,4 @@
-sleuthkit-java (4.6.7-1) unstable; urgency=medium
+sleuthkit-java (4.7.0-1) unstable; urgency=medium
 
   * Initial release (Closes: #nnnn)  <nnnn is the bug number of your ITP>
 
diff --git a/debian/sleuthkit-java.install b/debian/sleuthkit-java.install
index 6decacbed02f9d3439f8c42c05e5e2d1b999782d..fae334721d480b7b6bceee8fd712528460364408 100644
--- a/debian/sleuthkit-java.install
+++ b/debian/sleuthkit-java.install
@@ -1,3 +1,3 @@
 bindings/java/lib/sqlite-jdbc-3.25.2.jar /usr/share/java
-bindings/java/dist/sleuthkit-4.6.7.jar /usr/share/java
+bindings/java/dist/sleuthkit-4.7.0.jar /usr/share/java
 
diff --git a/packages/sleuthkit.spec b/packages/sleuthkit.spec
index 27fed9749c69b73a94f2d90fefb30bd72db51216..5ad496c55079cfc061bef7ca4a61d33719d038c3 100644
--- a/packages/sleuthkit.spec
+++ b/packages/sleuthkit.spec
@@ -1,5 +1,5 @@
 Name:		sleuthkit	
-Version:	4.6.7
+Version:	4.7.0
 Release:	1%{?dist}
 Summary:	The Sleuth Kit (TSK) is a library and collection of command line tools that allow you to investigate volume and file system data.	
 
diff --git a/tools/logicalimager/FileExtractor.cpp b/tools/logicalimager/FileExtractor.cpp
index 33e74de138976435fce85216c715a043ab045763..f6ae445d1705a1486d1da4e8fc9ff61bbbf26fca 100644
--- a/tools/logicalimager/FileExtractor.cpp
+++ b/tools/logicalimager/FileExtractor.cpp
@@ -75,8 +75,15 @@ TSK_RETVAL_ENUM FileExtractor::extractFile(TSK_FS_FILE *fs_file, const char *pat
         filename = m_rootDirectoryPath + "/" + extractedFilePath;
         file = _wfopen(TskHelper::toWide(filename).c_str(), L"wb");
         if (file == NULL) {
-            ReportUtil::consoleOutput(stderr, "ERROR: extractFile failed for %s, reason: %s\n", filename.c_str(), _strerror(NULL));
-            ReportUtil::handleExit(1);
+            // This can happen when the extension is invalid under Windows. Try again with no extension.
+            ReportUtil::consoleOutput(stderr, "ERROR: extractFile failed for %s, reason: %s\nTrying again with fixed file extension\n", filename.c_str(), _strerror(NULL));
+            extractedFilePath = getRootImageDirPrefix() + std::to_string(m_dirCounter) + "/f-" + std::to_string(m_fileCounter - 1);
+            filename = m_rootDirectoryPath + "/" + extractedFilePath;
+            file = _wfopen(TskHelper::toWide(filename).c_str(), L"wb");
+            if (file == NULL) {
+                ReportUtil::consoleOutput(stderr, "ERROR: extractFile failed for %s, reason: %s\n", filename.c_str(), _strerror(NULL));
+                ReportUtil::handleExit(1);
+            }
         }
         TskHelper::replaceAll(extractedFilePath, "/", "\\");
     }
@@ -87,11 +94,13 @@ TSK_RETVAL_ENUM FileExtractor::extractFile(TSK_FS_FILE *fs_file, const char *pat
             if (fs_file->meta) {
                 if (fs_file->meta->size == 0) {
                     // ts_fs_file_read returns -1 with empty files, don't report it.
-                    return TSK_OK;
+                    result = TSK_OK;
+                    break;
                 }
                 else if (fs_file->meta->flags & TSK_FS_NAME_FLAG_UNALLOC) {
                     // don't report it
-                    return TSK_ERR;
+                    result = TSK_ERR;
+                    break;
                 }
                 else {
                     ReportUtil::printDebug("extractFile: tsk_fs_file_read returns -1 filename=%s\toffset=%" PRIxOFF "\n", fs_file->name->name, offset);
diff --git a/tools/logicalimager/ReportUtil.cpp b/tools/logicalimager/ReportUtil.cpp
index bedad64e8677331129a6be069f0e60545d6c0e12..73590f47992d531ff8067a9f59c96292c169f13f 100644
--- a/tools/logicalimager/ReportUtil.cpp
+++ b/tools/logicalimager/ReportUtil.cpp
@@ -127,6 +127,16 @@ void ReportUtil::reportResult(const std::string &outputLocation, TSK_RETVAL_ENUM
     std::string mtimeStr = (fs_file->meta ? std::to_string(fs_file->meta->mtime) : "0");
     std::string atimeStr = (fs_file->meta ? std::to_string(fs_file->meta->atime) : "0");
     std::string ctimeStr = (fs_file->meta ? std::to_string(fs_file->meta->ctime) : "0");
+    std::string origFileName(fs_file->name ? fs_file->name->name : "name is null");
+    std::string origFilePath(path);
+
+    // Remove any newlines
+    origFileName.erase(std::remove(origFileName.begin(), origFileName.end(), '\n'), origFileName.end());
+    origFileName.erase(std::remove(origFileName.begin(), origFileName.end(), '\r'), origFileName.end());
+    origFilePath.erase(std::remove(origFilePath.begin(), origFilePath.end(), '\n'), origFilePath.end());
+    origFilePath.erase(std::remove(origFilePath.begin(), origFilePath.end(), '\r'), origFilePath.end());
+
+
     fprintf(reportFile, "%s\t%" PRIdOFF "\t%" PRIuINUM "\t%d\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
         outputLocation.c_str(),
         fs_file->fs_info->offset,
@@ -135,8 +145,8 @@ void ReportUtil::reportResult(const std::string &outputLocation, TSK_RETVAL_ENUM
         ruleMatchResult->getRuleSetName().c_str(),
         ruleMatchResult->getName().c_str(),
         ruleMatchResult->getDescription().c_str(),
-        (fs_file->name ? fs_file->name->name : "name is null"),
-        path,
+        origFileName.c_str(),
+        origFilePath.c_str(),
         extractedFilePath.c_str(),
         crtimeStr.c_str(),
         mtimeStr.c_str(),
diff --git a/tsk/Makefile.am b/tsk/Makefile.am
index 6a29e34e644aedfbf87283eeefc216085202766e..9be01c595a62f0b057c9fdbaedf19a8c22ceef77 100644
--- a/tsk/Makefile.am
+++ b/tsk/Makefile.am
@@ -8,6 +8,6 @@ libtsk_la_LIBADD = base/libtskbase.la img/libtskimg.la \
     vs/libtskvs.la fs/libtskfs.la hashdb/libtskhashdb.la \
     auto/libtskauto.la
 # current:revision:age
-libtsk_la_LDFLAGS = -version-info 18:0:5 $(LIBTSK_LDFLAGS)
+libtsk_la_LDFLAGS = -version-info 19:0:0 $(LIBTSK_LDFLAGS)
 
 EXTRA_DIST = tsk_tools_i.h docs/Doxyfile docs/*.dox docs/*.html
diff --git a/tsk/auto/db_postgresql.cpp b/tsk/auto/db_postgresql.cpp
index 7e724559204bf4306d53993058d69d32ee73f170..83653cf7b9b7b2ac695fbc8cbcf5ae3695deed60 100755
--- a/tsk/auto/db_postgresql.cpp
+++ b/tsk/auto/db_postgresql.cpp
@@ -657,20 +657,32 @@ int TskDbPostgreSQL::initialize() {
             "insert into tsk_event_types(event_type_id, display_name, super_type_id) values(7, 'Changed', 1);"
             , "Error initializing tsk_event_types table rows: %s\n") ||
         attempt_exec(
+			/*
+			* Regarding the timeline event tables schema, note that several columns
+			* in the tsk_event_descriptions table seem, at first glance, to be
+			* attributes of events rather than their descriptions and would appear
+			* to belong in tsk_events table instead. The rationale for putting the
+			* data source object ID, content object ID, artifact ID and the flags
+			* indicating whether or not the event source has a hash set hit or is
+			* tagged were motivated by the fact that these attributes are identical
+			* for each event in a set of file system file MAC time events. The
+			* decision was made to avoid duplication and save space by placing this
+			* data in the tsk_event-descriptions table.
+			*/
             "CREATE TABLE tsk_event_descriptions ( "
             " event_description_id BIGSERIAL PRIMARY KEY, "
             " full_description TEXT NOT NULL, "
             " med_description TEXT, "
             " short_description TEXT,"
             " data_source_obj_id BIGINT NOT NULL, "
-            " file_obj_id BIGINT NOT NULL, "
+            " content_obj_id BIGINT NOT NULL, "
             " artifact_id BIGINT, "
             " hash_hit INTEGER NOT NULL, " //boolean 
             " tagged INTEGER NOT NULL, " //boolean 
             " FOREIGN KEY(data_source_obj_id) REFERENCES data_source_info(obj_id), "
-            " FOREIGN KEY(file_obj_id) REFERENCES tsk_objects(obj_id), "
+            " FOREIGN KEY(content_obj_id) REFERENCES tsk_objects(obj_id), "
             " FOREIGN KEY(artifact_id) REFERENCES blackboard_artifacts(artifact_id) ,"
-			" UNIQUE (full_description, file_obj_id, artifact_id))",
+			" UNIQUE (full_description, content_obj_id, artifact_id))",
             "Error creating tsk_event_descriptions table: %s\n")
         ||
         attempt_exec(
@@ -751,8 +763,8 @@ int TskDbPostgreSQL::createIndexes() {
         //tsk_events indices
         attempt_exec("CREATE INDEX events_data_source_obj_id  ON tsk_event_descriptions(data_source_obj_id);",
             "Error creating events_data_source_obj_id index on tsk_event_descriptions: %s\n") ||
-        attempt_exec("CREATE INDEX events_file_obj_id  ON tsk_event_descriptions(file_obj_id);",
-            "Error creating events_file_obj_id index on tsk_event_descriptions: %s\n") ||
+        attempt_exec("CREATE INDEX events_content_obj_id  ON tsk_event_descriptions(content_obj_id);",
+            "Error creating events_content_obj_id index on tsk_event_descriptions: %s\n") ||
         attempt_exec("CREATE INDEX events_artifact_id  ON tsk_event_descriptions(artifact_id);",
             "Error creating events_artifact_id index on tsk_event_descriptions: %s\n") ||
         attempt_exec(
@@ -1063,7 +1075,7 @@ int TskDbPostgreSQL::addFsFile(TSK_FS_FILE * fs_file,
 }
 
 
-int TskDbPostgreSQL::addMACTimeEvents(char*& zSQL, const int64_t data_source_obj_id, const int64_t file_obj_id,
+int TskDbPostgreSQL::addMACTimeEvents(char*& zSQL, const int64_t data_source_obj_id, const int64_t content_obj_id,
                                       std::map<int64_t, time_t> timeMap, const char* full_description)
 {
     int64_t event_description_id = -1;
@@ -1082,17 +1094,17 @@ int TskDbPostgreSQL::addMACTimeEvents(char*& zSQL, const int64_t data_source_obj
         if (event_description_id == -1)
         {
             if (0 > snprintf(zSQL, 2048 - 1,
-                             "INSERT INTO tsk_event_descriptions ( data_source_obj_id, file_obj_id , artifact_id, full_description, hash_hit, tagged) "
+                             "INSERT INTO tsk_event_descriptions ( data_source_obj_id, content_obj_id , artifact_id, full_description, hash_hit, tagged) "
                              " VALUES ("
                              "%" PRId64 "," // data_source_obj_id
-                             "%" PRId64 "," // file_obj_id
+                             "%" PRId64 "," // content_obj_id
                              "NULL," // fixed artifact_id
                              "%s," // full_description
                              "0," // fixed hash_hit
                              "0" // fixed tagged
                              ") RETURNING event_description_id",
                              data_source_obj_id,
-                             file_obj_id,
+                             content_obj_id,
                              full_description))
             {
                 return 1;
diff --git a/tsk/auto/db_sqlite.cpp b/tsk/auto/db_sqlite.cpp
index 25e1c3a8a2756a1b2c8be2ad806f52aa81a1f26e..3f5d1598ad241130992ec531d37e466c85136938 100755
--- a/tsk/auto/db_sqlite.cpp
+++ b/tsk/auto/db_sqlite.cpp
@@ -424,17 +424,29 @@ TskDbSqlite::initialize()
 	        , "Error initializing event_types table rows: %s\n")
 	    ||
 	    attempt_exec(
+			/*
+			* Regarding the timeline event tables schema, note that several columns
+			* in the tsk_event_descriptions table seem, at first glance, to be
+			* attributes of events rather than their descriptions and would appear
+			* to belong in tsk_events table instead. The rationale for putting the
+			* data source object ID, content object ID, artifact ID and the flags
+			* indicating whether or not the event source has a hash set hit or is
+			* tagged were motivated by the fact that these attributes are identical
+			* for each event in a set of file system file MAC time events. The
+			* decision was made to avoid duplication and save space by placing this
+			* data in the tsk_event-descriptins table.
+			*/
 	        "CREATE TABLE tsk_event_descriptions ( "
 	        " event_description_id INTEGER PRIMARY KEY, "
 	        " full_description TEXT NOT NULL, "
 	        " med_description TEXT, "
 	        " short_description TEXT,"
 	        " data_source_obj_id INTEGER NOT NULL REFERENCES data_source_info(obj_id), "
-	        " file_obj_id INTEGER NOT NULL REFERENCES tsk_objects(obj_id), "
+	        " content_obj_id INTEGER NOT NULL REFERENCES tsk_objects(obj_id), "
 	        " artifact_id INTEGER REFERENCES blackboard_artifacts(artifact_id), "
 	        " hash_hit INTEGER NOT NULL, " //boolean 
 	        " tagged INTEGER NOT NULL, " //boolean 
-			" UNIQUE (full_description, file_obj_id, artifact_id))",
+			" UNIQUE (full_description, content_obj_id, artifact_id))",
 	        "Error creating tsk_event_event_types table: %4\n")
 	    ||
 	    attempt_exec(
@@ -518,8 +530,8 @@ int TskDbSqlite::createIndexes()
         //events indices
         attempt_exec("CREATE INDEX events_data_source_obj_id  ON tsk_event_descriptions(data_source_obj_id);",
                      "Error creating events_data_source_obj_id index on tsk_event_descriptions: %s\n") ||
-        attempt_exec("CREATE INDEX events_file_obj_id  ON tsk_event_descriptions(file_obj_id);",
-                     "Error creating events_file_obj_id index on tsk_event_descriptions: %s\n") ||
+        attempt_exec("CREATE INDEX events_content_obj_id  ON tsk_event_descriptions(content_obj_id);",
+                     "Error creating events_content_obj_id index on tsk_event_descriptions: %s\n") ||
         attempt_exec("CREATE INDEX events_artifact_id  ON tsk_event_descriptions(artifact_id);",
                      "Error creating events_artifact_id index on tsk_event_descriptions: %s\n") ||
         attempt_exec(
@@ -1000,7 +1012,7 @@ int64_t TskDbSqlite::findParObjId(const TSK_FS_FILE* fs_file, const char* parent
     return parObjId;
 }
 
-int TskDbSqlite::addMACTimeEvents(const int64_t data_source_obj_id, const int64_t file_obj_id,
+int TskDbSqlite::addMACTimeEvents(const int64_t data_source_obj_id, const int64_t content_obj_id,
                                   std::map<int64_t, time_t> timeMap, const char* full_description)
 {
     int64_t event_description_id = -1;
@@ -1020,17 +1032,17 @@ int TskDbSqlite::addMACTimeEvents(const int64_t data_source_obj_id, const int64_
         {
             //insert common description for file
             char* descriptionSql = sqlite3_mprintf(
-                "INSERT INTO tsk_event_descriptions ( data_source_obj_id, file_obj_id , artifact_id,  full_description, hash_hit, tagged) "
+                "INSERT INTO tsk_event_descriptions ( data_source_obj_id, content_obj_id , artifact_id,  full_description, hash_hit, tagged) "
                 " VALUES ("
                 "%" PRId64 "," // data_source_obj_id
-                "%" PRId64 "," // file_obj_id
+                "%" PRId64 "," // content_obj_id
                 "NULL," // fixed artifact_id
                 "%Q," // full_description
                 "0," // fixed hash_hit
                 "0" // fixed tagged
                 ")",
                 data_source_obj_id,
-                file_obj_id,
+                content_obj_id,
                 full_description);
 
             if (attempt_exec(descriptionSql,
diff --git a/tsk/base/tsk_base.h b/tsk/base/tsk_base.h
old mode 100755
new mode 100644
index 4cf24261c73fb592adcda40024b5dd084013f529..929beccd3dde94d7980b83ef6d73c3ca4b5448cc
--- a/tsk/base/tsk_base.h
+++ b/tsk/base/tsk_base.h
@@ -39,11 +39,11 @@
  * 3.1.2b1 would be 0x03010201.  Snapshot from Jan 2, 2003 would be
  * 0xFF030102.
  * See TSK_VERSION_STR for string form. */
-#define TSK_VERSION_NUM 0x040607ff
+#define TSK_VERSION_NUM 0x040700ff
 
 /** Version of code in string form. See TSK_VERSION_NUM for
  * integer form. */
-#define TSK_VERSION_STR "4.6.7"
+#define TSK_VERSION_STR "4.7.0"
 
 
 /* include the TSK-specific header file that we created in autoconf