diff --git a/bindings/java/src/org/sleuthkit/datamodel/CaseDatabaseFactory.java b/bindings/java/src/org/sleuthkit/datamodel/CaseDatabaseFactory.java
index ecd94704b74cdf989590cee7be892fc220c45ad5..0de142d73c1fe09aadcd10400dd84fb54fc23045 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/CaseDatabaseFactory.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/CaseDatabaseFactory.java
@@ -260,8 +260,10 @@ private void createArtifactTables(Statement stmt) throws SQLException {
 	}
 	
 	private void createTagTables(Statement stmt) throws SQLException {
+		stmt.execute("CREATE TABLE tsk_tag_sets (tag_set_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, name TEXT UNIQUE)");
 		stmt.execute("CREATE TABLE tag_names (tag_name_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, display_name TEXT UNIQUE, "
-				+ "description TEXT NOT NULL, color TEXT NOT NULL, knownStatus INTEGER NOT NULL)");
+				+ "description TEXT NOT NULL, color TEXT NOT NULL, knownStatus INTEGER NOT NULL,"
+				+ " tag_set_id INTEGER, FOREIGN KEY(tag_set_id) REFERENCES tsk_tag_sets(tag_set_id) ON DELETE SET NULL)");
 		
 		stmt.execute("CREATE TABLE tsk_examiners (examiner_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, "
 				+ "login_name TEXT NOT NULL, display_name TEXT, UNIQUE(login_name))");
diff --git a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
index 610046702d3cbaa0f2d740ae266b288d95ddd0e2..bd102bf42a5a61accdeeddf4d2efb054352778c5 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
@@ -97,7 +97,7 @@ public class SleuthkitCase {
 	 * tsk/auto/tsk_db.h.
 	 */
 	static final CaseDbSchemaVersionNumber CURRENT_DB_SCHEMA_VERSION
-			= new CaseDbSchemaVersionNumber(8, 4);
+			= new CaseDbSchemaVersionNumber(8, 5);
 
 	private static final long BASE_ARTIFACT_ID = Long.MIN_VALUE; // Artifact ids will start at the lowest negative value
 	private static final Logger logger = Logger.getLogger(SleuthkitCase.class.getName());
@@ -207,6 +207,7 @@ public class SleuthkitCase {
 	private TimelineManager timelineMgr;
 	private Blackboard blackboard;
 	private CaseDbAccessManager dbAccessManager;
+	private TaggingManager taggingMgr;
 
 	private final Map<String, Set<Long>> deviceIdToDatasourceObjIdMap = new HashMap<>();
 
@@ -375,6 +376,7 @@ private void init() throws Exception {
 		communicationsMgr = new CommunicationsManager(this);
 		timelineMgr = new TimelineManager(this);
 		dbAccessManager = new CaseDbAccessManager(this);
+		taggingMgr = new TaggingManager(this);
 	}
 
 	/**
@@ -477,6 +479,15 @@ public TimelineManager getTimelineManager() throws TskCoreException {
 	public synchronized CaseDbAccessManager getCaseDbAccessManager() throws TskCoreException {
 		return dbAccessManager;
 	}
+	
+	/**
+	 * Get the case database TaggingManager object.
+	 * 
+	 * @return The per case TaggingManager object. 
+	 */
+	public synchronized TaggingManager getTaggingManager() {
+		return taggingMgr;
+	}
 
 	/**
 	 * Make sure the predefined artifact types are in the artifact types table.
@@ -892,6 +903,7 @@ private void updateDatabaseSchema(String dbPath) throws Exception {
 				dbSchemaVersion = updateFromSchema8dot1toSchema8dot2(dbSchemaVersion, connection);
 				dbSchemaVersion = updateFromSchema8dot2toSchema8dot3(dbSchemaVersion, connection);
 				dbSchemaVersion = updateFromSchema8dot3toSchema8dot4(dbSchemaVersion, connection);
+				dbSchemaVersion = updateFromSchema8dot4toSchema8dot5(dbSchemaVersion, connection);
 				statement = connection.createStatement();
 				connection.executeUpdate(statement, "UPDATE tsk_db_info SET schema_ver = " + dbSchemaVersion.getMajor() + ", schema_minor_ver = " + dbSchemaVersion.getMinor()); //NON-NLS
 				connection.executeUpdate(statement, "UPDATE tsk_db_info_extended SET value = " + dbSchemaVersion.getMajor() + " WHERE name = '" + SCHEMA_MAJOR_VERSION_KEY + "'"); //NON-NLS
@@ -2084,6 +2096,58 @@ private CaseDbSchemaVersionNumber updateFromSchema8dot3toSchema8dot4(CaseDbSchem
 			releaseSingleUserCaseWriteLock();
 		}		
 	}
+	
+	private CaseDbSchemaVersionNumber updateFromSchema8dot4toSchema8dot5(CaseDbSchemaVersionNumber schemaVersion, CaseDbConnection connection) throws SQLException, TskCoreException {
+		if (schemaVersion.getMajor() != 8) {
+			return schemaVersion;
+		}
+
+		if (schemaVersion.getMinor() != 4) {
+			return schemaVersion;
+		}
+
+		Statement statement = connection.createStatement();
+		acquireSingleUserCaseWriteLock();
+		try {
+			switch (getDatabaseType()) {
+				case POSTGRESQL:
+					statement.execute("CREATE TABLE tsk_tag_sets (tag_set_id BIGSERIAL PRIMARY KEY, name TEXT UNIQUE)");
+					break;
+				case SQLITE:
+					statement.execute("CREATE TABLE tsk_tag_sets (tag_set_id INTEGER PRIMARY KEY, name TEXT UNIQUE)");
+					break;
+			}
+
+			statement.execute("ALTER TABLE tag_names ADD COLUMN tag_set_id INTEGER REFERENCES tsk_tag_sets(tag_set_id)");
+
+			String insertStmt = "INSERT INTO tsk_tag_sets (name) VALUES ('Project VIC (United States)')";
+			if (getDatabaseType() == DbType.POSTGRESQL) {
+				statement.execute(insertStmt, Statement.RETURN_GENERATED_KEYS);
+			} else {
+				statement.execute(insertStmt);
+			}
+			try (ResultSet resultSet = statement.getGeneratedKeys()) {
+				if (resultSet != null && resultSet.next()) {
+					int tagSetId = resultSet.getInt(1);
+
+					String updateQuery = "UPDATE tag_names SET tag_set_id = %d, color = '%s' WHERE display_name = '%s'";
+					statement.executeUpdate(String.format(updateQuery, tagSetId, "Red", "CAT-1: Child Exploitation (Illegal)"));
+					statement.executeUpdate(String.format(updateQuery, tagSetId, "Lime", "CAT-2: Child Exploitation (Non-Illegal/Age Difficult)"));
+					statement.executeUpdate(String.format(updateQuery, tagSetId, "Yellow", "CAT-3: CGI/Animation (Child Exploitive)"));
+					statement.executeUpdate(String.format(updateQuery, tagSetId, "Purple", "CAT-4: Exemplar/Comparison (Internal Use Only)"));
+					statement.executeUpdate(String.format(updateQuery, tagSetId, "Green", "CAT-5: Non-pertinent"));
+					statement.executeUpdate(String.format(updateQuery, tagSetId, "Silver", "CAT-0: Uncategorized"));
+				} else {
+					throw new TskCoreException("Failed to retrieve the default tag_set_id from DB");
+				}
+			}
+			return new CaseDbSchemaVersionNumber(8, 5);
+
+		} finally {
+			closeStatement(statement);
+			releaseSingleUserCaseWriteLock();
+		}
+	}
 
 	/**
 	 * Inserts a row for the given account type in account_types table, 
@@ -9549,11 +9613,11 @@ public List<TagName> getAllTagNames() throws TskCoreException {
 			// SELECT * FROM tag_names
 			PreparedStatement statement = connection.getPreparedStatement(PREPARED_STATEMENT.SELECT_TAG_NAMES);
 			resultSet = connection.executeQuery(statement);
-			ArrayList<TagName> tagNames = new ArrayList<TagName>();
+			ArrayList<TagName> tagNames = new ArrayList<>();
 			while (resultSet.next()) {
 				tagNames.add(new TagName(resultSet.getLong("tag_name_id"), resultSet.getString("display_name"),
 						resultSet.getString("description"), TagName.HTML_COLOR.getColorByName(resultSet.getString("color")),
-						TskData.FileKnown.valueOf(resultSet.getByte("knownStatus")))); //NON-NLS
+						TskData.FileKnown.valueOf(resultSet.getByte("knownStatus")), resultSet.getInt("tag_set_id"))); //NON-NLS
 			}
 			return tagNames;
 		} catch (SQLException ex) {
@@ -9583,11 +9647,11 @@ public List<TagName> getTagNamesInUse() throws TskCoreException {
 			// SELECT * FROM tag_names WHERE tag_name_id IN (SELECT tag_name_id from content_tags UNION SELECT tag_name_id FROM blackboard_artifact_tags)
 			PreparedStatement statement = connection.getPreparedStatement(PREPARED_STATEMENT.SELECT_TAG_NAMES_IN_USE);
 			resultSet = connection.executeQuery(statement);
-			ArrayList<TagName> tagNames = new ArrayList<TagName>();
+			ArrayList<TagName> tagNames = new ArrayList<>();
 			while (resultSet.next()) {
 				tagNames.add(new TagName(resultSet.getLong("tag_name_id"), resultSet.getString("display_name"),
 						resultSet.getString("description"), TagName.HTML_COLOR.getColorByName(resultSet.getString("color")),
-						TskData.FileKnown.valueOf(resultSet.getByte("knownStatus")))); //NON-NLS
+						TskData.FileKnown.valueOf(resultSet.getByte("knownStatus")), resultSet.getLong("tag_set_id"))); //NON-NLS
 			}
 			return tagNames;
 		} catch (SQLException ex) {
@@ -9631,7 +9695,7 @@ public List<TagName> getTagNamesInUse(long dsObjId) throws TskCoreException {
 			while (resultSet.next()) {
 				tagNames.add(new TagName(resultSet.getLong("tag_name_id"), resultSet.getString("display_name"),
 						resultSet.getString("description"), TagName.HTML_COLOR.getColorByName(resultSet.getString("color")),
-						TskData.FileKnown.valueOf(resultSet.getByte("knownStatus")))); //NON-NLS
+						TskData.FileKnown.valueOf(resultSet.getByte("knownStatus")), resultSet.getLong("tag_set_id"))); //NON-NLS
 			}
 			return tagNames;
 		} catch (SQLException ex) {
@@ -9695,7 +9759,7 @@ public TagName addOrUpdateTagName(String displayName, String description, TagNam
 			resultSet = statement.getGeneratedKeys();
 			resultSet.next();
 			return new TagName(resultSet.getLong(1), //last_insert_rowid()
-					displayName, description, color, knownStatus);
+					displayName, description, color, knownStatus, 0);
 		} catch (SQLException ex) {
 			throw new TskCoreException("Error adding row for " + displayName + " tag name to tag_names table", ex);
 		} finally {
@@ -9717,34 +9781,11 @@ public TagName addOrUpdateTagName(String displayName, String description, TagNam
 	 * @return A ContentTag data transfer object (DTO) for the new row.
 	 *
 	 * @throws TskCoreException
+	 * @deprecated Use TaggingManager.addContentTag 
 	 */
+	@Deprecated
 	public ContentTag addContentTag(Content content, TagName tagName, String comment, long beginByteOffset, long endByteOffset) throws TskCoreException {
-		CaseDbConnection connection = connections.getConnection();
-		acquireSingleUserCaseWriteLock();
-		ResultSet resultSet = null;
-		try {
-			Examiner currentExaminer = getCurrentExaminer();
-			// INSERT INTO content_tags (obj_id, tag_name_id, comment, begin_byte_offset, end_byte_offset, examiner_id) VALUES (?, ?, ?, ?, ?, ?)
-			PreparedStatement statement = connection.getPreparedStatement(PREPARED_STATEMENT.INSERT_CONTENT_TAG, Statement.RETURN_GENERATED_KEYS);
-			statement.clearParameters();
-			statement.setLong(1, content.getId());
-			statement.setLong(2, tagName.getId());
-			statement.setString(3, comment);
-			statement.setLong(4, beginByteOffset);
-			statement.setLong(5, endByteOffset);
-			statement.setLong(6, currentExaminer.getId());
-			connection.executeUpdate(statement);
-			resultSet = statement.getGeneratedKeys();
-			resultSet.next();
-			return new ContentTag(resultSet.getLong(1), //last_insert_rowid()
-					content, tagName, comment, beginByteOffset, endByteOffset, currentExaminer.getLoginName());
-		} catch (SQLException ex) {
-			throw new TskCoreException("Error adding row to content_tags table (obj_id = " + content.getId() + ", tag_name_id = " + tagName.getId() + ")", ex);
-		} finally {
-			closeResultSet(resultSet);
-			connection.close();
-			releaseSingleUserCaseWriteLock();
-		}
+		return taggingMgr.addContentTag(content, tagName, comment, beginByteOffset, endByteOffset).getAddedTag();
 	}
 
 	/*
@@ -9792,7 +9833,7 @@ public List<ContentTag> getAllContentTags() throws TskCoreException {
 			while (resultSet.next()) {
 				TagName tagName = new TagName(resultSet.getLong("tag_name_id"), resultSet.getString("display_name"),
 						resultSet.getString("description"), TagName.HTML_COLOR.getColorByName(resultSet.getString("color")),
-						TskData.FileKnown.valueOf(resultSet.getByte("knownStatus")));  //NON-NLS
+						TskData.FileKnown.valueOf(resultSet.getByte("knownStatus")), resultSet.getLong("tag_set_id"));  //NON-NLS
 				Content content = getContentById(resultSet.getLong("obj_id")); //NON-NLS
 				tags.add(new ContentTag(resultSet.getLong("tag_id"), content, tagName, resultSet.getString("comment"),
 						resultSet.getLong("begin_byte_offset"), resultSet.getLong("end_byte_offset"), resultSet.getString("login_name")));  //NON-NLS
@@ -9922,7 +9963,7 @@ public ContentTag getContentTagByID(long contentTagID) throws TskCoreException {
 			while (resultSet.next()) {
 				TagName tagName = new TagName(resultSet.getLong("tag_name_id"), resultSet.getString("display_name"),
 						resultSet.getString("description"), TagName.HTML_COLOR.getColorByName(resultSet.getString("color")),
-						TskData.FileKnown.valueOf(resultSet.getByte("knownStatus")));
+						TskData.FileKnown.valueOf(resultSet.getByte("knownStatus")), resultSet.getLong("tag_set_id"));
 				tag = new ContentTag(resultSet.getLong("tag_id"), getContentById(resultSet.getLong("obj_id")), tagName,
 						resultSet.getString("comment"), resultSet.getLong("begin_byte_offset"), resultSet.getLong("end_byte_offset"), resultSet.getString("login_name"));
 			}
@@ -10058,7 +10099,7 @@ public List<ContentTag> getContentTagsByContent(Content content) throws TskCoreE
 			while (resultSet.next()) {
 				TagName tagName = new TagName(resultSet.getLong("tag_name_id"), resultSet.getString("display_name"),
 						resultSet.getString("description"), TagName.HTML_COLOR.getColorByName(resultSet.getString("color")),
-						TskData.FileKnown.valueOf(resultSet.getByte("knownStatus")));  //NON-NLS
+						TskData.FileKnown.valueOf(resultSet.getByte("knownStatus")), resultSet.getLong("tag_set_id"));  //NON-NLS
 				ContentTag tag = new ContentTag(resultSet.getLong("tag_id"), content, tagName,
 						resultSet.getString("comment"), resultSet.getLong("begin_byte_offset"), resultSet.getLong("end_byte_offset"), resultSet.getString("login_name"));  //NON-NLS
 				tags.add(tag);
@@ -10085,32 +10126,11 @@ public List<ContentTag> getContentTagsByContent(Content content) throws TskCoreE
 	 *         row.
 	 *
 	 * @throws TskCoreException
+	 * @Deprecated User TaggingManager.addArtifactTag instead.
 	 */
+	@Deprecated
 	public BlackboardArtifactTag addBlackboardArtifactTag(BlackboardArtifact artifact, TagName tagName, String comment) throws TskCoreException {
-		CaseDbConnection connection = connections.getConnection();
-		acquireSingleUserCaseWriteLock();
-		ResultSet resultSet = null;
-		try {
-			Examiner currentExaminer = getCurrentExaminer();
-			// "INSERT INTO blackboard_artifact_tags (artifact_id, tag_name_id, comment, examiner_id) VALUES (?, ?, ?, ?)"), //NON-NLS
-			PreparedStatement statement = connection.getPreparedStatement(PREPARED_STATEMENT.INSERT_ARTIFACT_TAG, Statement.RETURN_GENERATED_KEYS);
-			statement.clearParameters();
-			statement.setLong(1, artifact.getArtifactID());
-			statement.setLong(2, tagName.getId());
-			statement.setString(3, comment);
-			statement.setLong(4, currentExaminer.getId());
-			connection.executeUpdate(statement);
-			resultSet = statement.getGeneratedKeys();
-			resultSet.next();
-			return new BlackboardArtifactTag(resultSet.getLong(1), //last_insert_rowid()
-					artifact, getContentById(artifact.getObjectID()), tagName, comment, currentExaminer.getLoginName());
-		} catch (SQLException ex) {
-			throw new TskCoreException("Error adding row to blackboard_artifact_tags table (obj_id = " + artifact.getArtifactID() + ", tag_name_id = " + tagName.getId() + ")", ex);
-		} finally {
-			closeResultSet(resultSet);
-			connection.close();
-			releaseSingleUserCaseWriteLock();
-		}
+		return taggingMgr.addArtifactTag(artifact, tagName, comment).getAddedTag();
 	}
 
 	/*
@@ -10155,11 +10175,11 @@ public List<BlackboardArtifactTag> getAllBlackboardArtifactTags() throws TskCore
 			//	LEFT OUTER JOIN tsk_examiners ON blackboard_artifact_tags.examiner_id = tsk_examiners.examiner_id
 			PreparedStatement statement = connection.getPreparedStatement(PREPARED_STATEMENT.SELECT_ARTIFACT_TAGS);
 			resultSet = connection.executeQuery(statement);
-			ArrayList<BlackboardArtifactTag> tags = new ArrayList<BlackboardArtifactTag>();
+			ArrayList<BlackboardArtifactTag> tags = new ArrayList<>();
 			while (resultSet.next()) {
 				TagName tagName = new TagName(resultSet.getLong("tag_name_id"), resultSet.getString("display_name"),
 						resultSet.getString("description"), TagName.HTML_COLOR.getColorByName(resultSet.getString("color")),
-						TskData.FileKnown.valueOf(resultSet.getByte("knownStatus")));  //NON-NLS
+						TskData.FileKnown.valueOf(resultSet.getByte("knownStatus")), resultSet.getLong("tag_set_id"));  //NON-NLS
 				BlackboardArtifact artifact = getBlackboardArtifact(resultSet.getLong("artifact_id")); //NON-NLS
 				Content content = getContentById(artifact.getObjectID());
 				BlackboardArtifactTag tag = new BlackboardArtifactTag(resultSet.getLong("tag_id"),
@@ -10389,7 +10409,7 @@ public BlackboardArtifactTag getBlackboardArtifactTagByID(long artifactTagID) th
 			while (resultSet.next()) {
 				TagName tagName = new TagName(resultSet.getLong("tag_name_id"), resultSet.getString("display_name"),
 						resultSet.getString("description"), TagName.HTML_COLOR.getColorByName(resultSet.getString("color")),
-						TskData.FileKnown.valueOf(resultSet.getByte("knownStatus")));
+						TskData.FileKnown.valueOf(resultSet.getByte("knownStatus")), resultSet.getLong("tag_set_id"));
 				BlackboardArtifact artifact = getBlackboardArtifact(resultSet.getLong("artifact_id")); //NON-NLS
 				Content content = getContentById(artifact.getObjectID());
 				tag = new BlackboardArtifactTag(resultSet.getLong("tag_id"),
@@ -10433,11 +10453,11 @@ public List<BlackboardArtifactTag> getBlackboardArtifactTagsByArtifact(Blackboar
 			statement.clearParameters();
 			statement.setLong(1, artifact.getArtifactID());
 			resultSet = connection.executeQuery(statement);
-			ArrayList<BlackboardArtifactTag> tags = new ArrayList<BlackboardArtifactTag>();
+			ArrayList<BlackboardArtifactTag> tags = new ArrayList<>();
 			while (resultSet.next()) {
 				TagName tagName = new TagName(resultSet.getLong("tag_name_id"), resultSet.getString("display_name"),
 						resultSet.getString("description"), TagName.HTML_COLOR.getColorByName(resultSet.getString("color")),
-						TskData.FileKnown.valueOf(resultSet.getByte("knownStatus")));  //NON-NLS
+						TskData.FileKnown.valueOf(resultSet.getByte("knownStatus")), resultSet.getLong("tag_set_id"));  //NON-NLS
 				Content content = getContentById(artifact.getObjectID());
 				BlackboardArtifactTag tag = new BlackboardArtifactTag(resultSet.getLong("tag_id"),
 						artifact, content, tagName, resultSet.getString("comment"), resultSet.getString("login_name"));  //NON-NLS
@@ -11543,7 +11563,7 @@ private enum PREPARED_STATEMENT {
 				+ " AND content_tags.tag_name_id = ? "
 				+ " AND tsk_files.data_source_obj_id = ? "
 		),
-		SELECT_CONTENT_TAGS("SELECT content_tags.tag_id, content_tags.obj_id, content_tags.tag_name_id, content_tags.comment, content_tags.begin_byte_offset, content_tags.end_byte_offset, tag_names.display_name, tag_names.description, tag_names.color, tag_names.knownStatus, tsk_examiners.login_name "
+		SELECT_CONTENT_TAGS("SELECT content_tags.tag_id, content_tags.obj_id, content_tags.tag_name_id, content_tags.comment, content_tags.begin_byte_offset, content_tags.end_byte_offset, tag_names.display_name, tag_names.description, tag_names.color, tag_names.knownStatus, tsk_examiners.login_name, tag_names.tag_set_id "
 				+ "FROM content_tags "
 				+ "INNER JOIN tag_names ON content_tags.tag_name_id = tag_names.tag_name_id "
 				+ "LEFT OUTER JOIN tsk_examiners ON content_tags.examiner_id = tsk_examiners.examiner_id"), //NON-NLS
@@ -11551,19 +11571,19 @@ private enum PREPARED_STATEMENT {
 				+ "FROM content_tags "
 				+ "LEFT OUTER JOIN tsk_examiners ON content_tags.examiner_id = tsk_examiners.examiner_id "
 				+ "WHERE tag_name_id = ?"), //NON-NLS
-		SELECT_CONTENT_TAGS_BY_TAG_NAME_BY_DATASOURCE("SELECT content_tags.tag_id, content_tags.obj_id, content_tags.tag_name_id, content_tags.comment, content_tags.begin_byte_offset, content_tags.end_byte_offset, tag_names.display_name, tag_names.description, tag_names.color, tag_names.knownStatus, tsk_examiners.login_name "
+		SELECT_CONTENT_TAGS_BY_TAG_NAME_BY_DATASOURCE("SELECT content_tags.tag_id, content_tags.obj_id, content_tags.tag_name_id, content_tags.comment, content_tags.begin_byte_offset, content_tags.end_byte_offset, tag_names.display_name, tag_names.description, tag_names.color, tag_names.knownStatus, tsk_examiners.login_name, tag_names.tag_set_id "
 				+ "FROM content_tags as content_tags, tsk_files as tsk_files, tag_names as tag_names, tsk_examiners as tsk_examiners "
 				+ "WHERE content_tags.examiner_id = tsk_examiners.examiner_id"
 				+ " AND content_tags.obj_id = tsk_files.obj_id"
 				+ " AND content_tags.tag_name_id = tag_names.tag_name_id"
 				+ " AND content_tags.tag_name_id = ?"
 				+ " AND tsk_files.data_source_obj_id = ? "),
-		SELECT_CONTENT_TAG_BY_ID("SELECT content_tags.tag_id, content_tags.obj_id, content_tags.tag_name_id, content_tags.comment, content_tags.begin_byte_offset, content_tags.end_byte_offset, tag_names.display_name, tag_names.description, tag_names.color, tag_names.knownStatus, tsk_examiners.login_name "
+		SELECT_CONTENT_TAG_BY_ID("SELECT content_tags.tag_id, content_tags.obj_id, content_tags.tag_name_id, content_tags.comment, content_tags.begin_byte_offset, content_tags.end_byte_offset, tag_names.display_name, tag_names.description, tag_names.color, tag_names.knownStatus, tsk_examiners.login_name, tag_names.tag_set_id "
 				+ "FROM content_tags "
 				+ "INNER JOIN tag_names ON content_tags.tag_name_id = tag_names.tag_name_id "
 				+ "LEFT OUTER JOIN tsk_examiners ON content_tags.examiner_id = tsk_examiners.examiner_id "
 				+ "WHERE tag_id = ?"), //NON-NLS
-		SELECT_CONTENT_TAGS_BY_CONTENT("SELECT content_tags.tag_id, content_tags.obj_id, content_tags.tag_name_id, content_tags.comment, content_tags.begin_byte_offset, content_tags.end_byte_offset, tag_names.display_name, tag_names.description, tag_names.color, tag_names.knownStatus, tsk_examiners.login_name "
+		SELECT_CONTENT_TAGS_BY_CONTENT("SELECT content_tags.tag_id, content_tags.obj_id, content_tags.tag_name_id, content_tags.comment, content_tags.begin_byte_offset, content_tags.end_byte_offset, tag_names.display_name, tag_names.description, tag_names.color, tag_names.knownStatus, tsk_examiners.login_name, tag_names.tag_set_id "
 				+ "FROM content_tags "
 				+ "INNER JOIN tag_names ON content_tags.tag_name_id = tag_names.tag_name_id "
 				+ "LEFT OUTER JOIN tsk_examiners ON content_tags.examiner_id = tsk_examiners.examiner_id "
@@ -11589,12 +11609,12 @@ private enum PREPARED_STATEMENT {
 				+ " AND artifact_tags.artifact_id = arts.artifact_id"
 				+ " AND artifact_tags.tag_name_id = ? "
 				+ " AND arts.data_source_obj_id =  ? "),
-		SELECT_ARTIFACT_TAG_BY_ID("SELECT blackboard_artifact_tags.tag_id, blackboard_artifact_tags.artifact_id, blackboard_artifact_tags.tag_name_id, blackboard_artifact_tags.comment, tag_names.display_name, tag_names.description, tag_names.color, tag_names.knownStatus, tsk_examiners.login_name "
+		SELECT_ARTIFACT_TAG_BY_ID("SELECT blackboard_artifact_tags.tag_id, blackboard_artifact_tags.artifact_id, blackboard_artifact_tags.tag_name_id, blackboard_artifact_tags.comment, tag_names.display_name, tag_names.description, tag_names.color, tag_names.knownStatus, tsk_examiners.login_name, tag_names.tag_set_id "
 				+ "FROM blackboard_artifact_tags "
 				+ "INNER JOIN tag_names ON blackboard_artifact_tags.tag_name_id = tag_names.tag_name_id  "
 				+ "LEFT OUTER JOIN tsk_examiners ON blackboard_artifact_tags.examiner_id = tsk_examiners.examiner_id "
 				+ "WHERE blackboard_artifact_tags.tag_id = ?"), //NON-NLS
-		SELECT_ARTIFACT_TAGS_BY_ARTIFACT("SELECT blackboard_artifact_tags.tag_id, blackboard_artifact_tags.artifact_id, blackboard_artifact_tags.tag_name_id, blackboard_artifact_tags.comment, tag_names.display_name, tag_names.description, tag_names.color, tag_names.knownStatus, tsk_examiners.login_name "
+		SELECT_ARTIFACT_TAGS_BY_ARTIFACT("SELECT blackboard_artifact_tags.tag_id, blackboard_artifact_tags.artifact_id, blackboard_artifact_tags.tag_name_id, blackboard_artifact_tags.comment, tag_names.display_name, tag_names.description, tag_names.color, tag_names.knownStatus, tsk_examiners.login_name, tag_names.tag_set_id "
 				+ "FROM blackboard_artifact_tags "
 				+ "INNER JOIN tag_names ON blackboard_artifact_tags.tag_name_id = tag_names.tag_name_id "
 				+ "LEFT OUTER JOIN tsk_examiners ON blackboard_artifact_tags.examiner_id = tsk_examiners.examiner_id "
diff --git a/bindings/java/src/org/sleuthkit/datamodel/TagName.java b/bindings/java/src/org/sleuthkit/datamodel/TagName.java
index fdc729568ed53bf0e48f8de21ba284832daef20d..e404faeeb36d399690354c181b73467332b6b9df 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/TagName.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/TagName.java
@@ -80,14 +80,16 @@ public static HTML_COLOR getColorByName(String colorName) {
 	private final String description;
 	private final HTML_COLOR color;
 	private final TskData.FileKnown knownStatus;
-		
+	private final long tagSetId;
+
 	// Clients of the org.sleuthkit.datamodel package should not directly create these objects.
-	TagName(long id, String displayName, String description, HTML_COLOR color, TskData.FileKnown knownStatus) {
+	TagName(long id, String displayName, String description, HTML_COLOR color, TskData.FileKnown knownStatus, long tagSetId) {
 		this.id = id;
 		this.displayName = displayName;
 		this.description = description;
 		this.color = color;
 		this.knownStatus = knownStatus;
+		this.tagSetId = tagSetId;
 	}
 
 	public long getId() {
@@ -110,6 +112,10 @@ public TskData.FileKnown getKnownStatus() {
 		return knownStatus;
 	}
 
+	long getTagSetId() {
+		return tagSetId;
+	}
+
 	/**
 	 * Compares two TagName objects by comparing their display names.
 	 *
@@ -130,6 +136,7 @@ public int hashCode() {
 		hash = 89 * hash + (this.description != null ? this.description.hashCode() : 0);
 		hash = 89 * hash + (this.color != null ? this.color.hashCode() : 0);
 		hash = 89 * hash + (this.knownStatus != null ? this.knownStatus.hashCode() : 0);
+		hash = 89 * hash + (int) (this.id ^ (this.tagSetId >>> 32));
 		return hash;
 	}
 
@@ -146,6 +153,7 @@ public boolean equals(Object obj) {
 				&& Objects.equals(this.displayName, other.displayName)
 				&& Objects.equals(this.description, other.description)
 				&& Objects.equals(this.color, other.color)
-				&& Objects.equals(this.knownStatus, other.knownStatus));
+				&& Objects.equals(this.knownStatus, other.knownStatus)
+				&& this.tagSetId == other.tagSetId);
 	}
 }
diff --git a/bindings/java/src/org/sleuthkit/datamodel/TagSet.java b/bindings/java/src/org/sleuthkit/datamodel/TagSet.java
new file mode 100755
index 0000000000000000000000000000000000000000..2821aff4d08f8534f93babfa886f1786b114b908
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/TagSet.java
@@ -0,0 +1,103 @@
+/*
+ * Sleuth Kit Data Model
+ *
+ * Copyright 2020 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.datamodel;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A TagSet is a named group of TagNames.
+ */
+public class TagSet {
+
+	private final String setName;
+	private final long id;
+	private final List<TagName> tagNameList;
+
+	/**
+	 * Construct a TagSet.
+	 *
+	 * @param id		    Tag set id value.
+	 * @param setName	Name of tag set.
+	 */
+	TagSet(long id, String setName, List<TagName> tagNameList) {
+		if (setName == null || setName.isEmpty()) {
+			throw new IllegalArgumentException("TagSet name must be a non-empty string");
+		}
+		this.tagNameList = tagNameList;
+		this.id = id;
+		this.setName = setName;
+	}
+
+	/**
+	 * Returns the name of the tag set.
+	 *
+	 * @return Tag set name.
+	 */
+	public String getName() {
+		return setName;
+	}
+
+	/**
+	 * Returns a list of the TagName objects that belong to the tag set.
+	 *
+	 * @return An unmodifiable list of TagName objects.
+	 */
+	public List<TagName> getTagNames() {
+		return Collections.unmodifiableList(tagNameList);
+	}
+
+	/**
+	 * Return the TagSet id.
+	 *
+	 * @return TagSet id value.
+	 */
+	long getId() {
+		return id;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (obj == null) {
+			return false;
+		}
+
+		if (getClass() != obj.getClass()) {
+			return false;
+		}
+
+		final TagSet other = (TagSet) obj;
+
+		return (this.id == other.getId()
+				&& setName.equals(other.getName())
+				&& tagNameList.equals(other.tagNameList));
+	}
+
+	@Override
+	public int hashCode() {
+		int hash = 5;
+		hash = 89 * hash + (int) (this.id ^ (this.id >>> 32));
+		hash = 89 * hash + Objects.hashCode(this.setName);
+		hash = 89 * hash + Objects.hashCode(this.tagNameList);
+
+		return hash;
+	}
+
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/TaggingManager.java b/bindings/java/src/org/sleuthkit/datamodel/TaggingManager.java
new file mode 100755
index 0000000000000000000000000000000000000000..6ff241ebee6b196484eee66f887e91f16a7b0da0
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/TaggingManager.java
@@ -0,0 +1,515 @@
+/*
+ * Sleuth Kit Data Model
+ *
+ * Copyright 2020 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.datamodel;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import org.sleuthkit.datamodel.SleuthkitCase.CaseDbConnection;
+import static org.sleuthkit.datamodel.TskData.DbType.POSTGRESQL;
+
+/**
+ * Provides an API to manage Tags.
+ */
+public class TaggingManager {
+
+	private final SleuthkitCase skCase;
+
+	/**
+	 * Construct a TaggingManager for the given SleuthkitCase.
+	 *
+	 * @param skCase The SleuthkitCase.
+	 */
+	TaggingManager(SleuthkitCase skCase) {
+		this.skCase = skCase;
+	}
+
+	/**
+	 * Returns a list of all the TagSets that exist in the case.
+	 *
+	 * @return A List of TagSet objects or an empty list if none were found.
+	 *
+	 * @throws TskCoreException
+	 */
+	public List<TagSet> getTagSets() throws TskCoreException {
+		List<TagSet> tagSetList = new ArrayList<>();
+		CaseDbConnection connection = skCase.getConnection();
+		skCase.acquireSingleUserCaseReadLock();
+		String getAllTagSetsQuery = "SELECT * FROM tsk_tag_sets";
+		try (Statement stmt = connection.createStatement(); ResultSet resultSet = stmt.executeQuery(getAllTagSetsQuery);) {
+			while (resultSet.next()) {
+				int setID = resultSet.getInt("tag_set_id");
+				String setName = resultSet.getString("name");
+				TagSet set = new TagSet(setID, setName, getTagNamesByTagSetID(setID));
+				tagSetList.add(set);
+			}
+		} catch (SQLException ex) {
+			throw new TskCoreException("Error occurred getting TagSet list.", ex);
+		} finally {
+			connection.close();
+			skCase.releaseSingleUserCaseReadLock();
+		}
+		return tagSetList;
+	}
+
+	/**
+	 * Inserts a row into the tag_sets table in the case database.
+	 *
+	 * @param name     The tag set name.
+	 * @param tagNames
+	 *
+	 * @return A TagSet object for the new row.
+	 *
+	 * @throws TskCoreException
+	 */
+	public TagSet addTagSet(String name, List<TagName> tagNames) throws TskCoreException {
+		if (name == null || name.isEmpty()) {
+			throw new IllegalArgumentException("Error adding TagSet, TagSet name must be non-empty string.");
+		}
+
+		TagSet tagSet = null;
+
+		CaseDbConnection connection = skCase.getConnection();
+		skCase.acquireSingleUserCaseWriteLock();
+		try (Statement stmt = connection.createStatement()) {
+			connection.beginTransaction();
+			String query = String.format("INSERT INTO tsk_tag_sets (name) VALUES('%s')", name);
+
+			if (skCase.getDatabaseType() == POSTGRESQL) {
+				stmt.execute(query, Statement.RETURN_GENERATED_KEYS);
+			} else {
+				stmt.execute(query);
+			}
+
+			try (ResultSet resultSet = stmt.getGeneratedKeys()) {
+
+				resultSet.next();
+				int setID = resultSet.getInt(1);
+
+				List<TagName> updatedTags = new ArrayList<>();
+				if (tagNames != null) {
+					// Get all of the TagName ids they can be updated in one
+					// SQL call.
+					List<String> idList = new ArrayList<>();
+					for (TagName tagName : tagNames) {
+						idList.add(Long.toString(tagName.getId()));
+					}
+
+					stmt.executeUpdate(String.format("UPDATE tag_names SET tag_set_id = %d WHERE tag_name_id IN (%s)", setID, String.join(",", idList)));
+
+					for (TagName tagName : tagNames) {
+						updatedTags.add(new TagName(tagName.getId(),
+								tagName.getDisplayName(),
+								tagName.getDescription(),
+								tagName.getColor(),
+								tagName.getKnownStatus(),
+								setID));
+					}
+				}
+				tagSet = new TagSet(setID, name, updatedTags);
+			}
+			connection.commitTransaction();
+		} catch (SQLException ex) {
+			connection.rollbackTransaction();
+			throw new TskCoreException(String.format("Error adding tag set %s", name), ex);
+		} finally {
+			connection.close();
+			skCase.releaseSingleUserCaseWriteLock();
+		}
+
+		return tagSet;
+	}
+
+	/**
+	 * Remove a row from the tag set table. The TagNames in the TagSet will not
+	 * be deleted, nor will any tags with the TagNames from the deleted tag set
+	 * be deleted.
+	 *
+	 * @param tagSet TagSet to be deleted
+	 *
+	 * @throws TskCoreException
+	 */
+	public void deleteTagSet(TagSet tagSet) throws TskCoreException {
+		if (tagSet == null) {
+			throw new IllegalArgumentException("Error adding deleting TagSet, TagSet object was null");
+		}
+
+		CaseDbConnection connection = skCase.getConnection();
+		skCase.acquireSingleUserCaseWriteLock();
+		try (Statement stmt = connection.createStatement()) {
+			connection.beginTransaction();
+			String queryTemplate = "DELETE FROM tsk_tag_sets WHERE tag_set_id = '%d'";
+			stmt.execute(String.format(queryTemplate, tagSet.getId()));
+			connection.commitTransaction();
+		} catch (SQLException ex) {
+			connection.rollbackTransaction();
+			throw new TskCoreException(String.format("Error deleting tag set where id = %d.", tagSet.getId()), ex);
+		} finally {
+			connection.close();
+			skCase.releaseSingleUserCaseWriteLock();
+		}
+	}
+
+	/**
+	 * Add the given TagName to the TagSet.
+	 *
+	 * @param tagSet	 The tag set being added to.
+	 * @param tagName	The tag name to add to the set.
+	 *
+	 * @return TagSet	TagSet object with newly added TagName.
+	 *
+	 * @throws TskCoreException
+	 */
+	public TagSet addTagNameToTagSet(TagSet tagSet, TagName tagName) throws TskCoreException {
+		if (tagSet == null || tagName == null) {
+			throw new IllegalArgumentException("NULL value passed to addTagToTagSet");
+		}
+
+		// Make sure the tagName is not already in the list.
+		List<TagName> setTagNameList = tagSet.getTagNames();
+		for (TagName tag : setTagNameList) {
+			if (tagName.getId() == tag.getId()) {
+				return tagSet;
+			}
+		}
+
+		CaseDbConnection connection = skCase.getConnection();
+		skCase.acquireSingleUserCaseWriteLock();
+
+		try (Statement stmt = connection.createStatement()) {
+			connection.beginTransaction();
+
+			String queryTemplate = "UPDATE tag_names SET tag_set_id = %d where tag_name_id = %d";
+			stmt.executeUpdate(String.format(queryTemplate, tagSet.getId(), tagName.getId()));
+
+			connection.commitTransaction();
+
+			List<TagName> newTagNameList = new ArrayList<>();
+			newTagNameList.addAll(setTagNameList);
+			newTagNameList.add(new TagName(tagName.getId(), tagName.getDisplayName(), tagName.getDescription(), tagName.getColor(), tagName.getKnownStatus(), tagSet.getId()));
+
+			return new TagSet(tagSet.getId(), tagSet.getName(), newTagNameList);
+
+		} catch (SQLException ex) {
+			connection.rollbackTransaction();
+			throw new TskCoreException(String.format("Error adding TagName (id=%d) to TagSet (id=%s)", tagName.getId(), tagSet.getId()), ex);
+		} finally {
+			connection.close();
+			skCase.releaseSingleUserCaseWriteLock();
+		}
+	}
+
+	/**
+	 * Inserts a row into the blackboard_artifact_tags table in the case
+	 * database.
+	 *
+	 * @param artifact The blackboard artifact to tag.
+	 * @param tagName  The name to use for the tag.
+	 * @param comment  A comment to store with the tag.
+	 *
+	 * @return A BlackboardArtifactTag data transfer object (DTO) for the new
+	 *         row.
+	 *
+	 * @throws TskCoreException
+	 */
+	public BlackboardArtifactTagChange addArtifactTag(BlackboardArtifact artifact, TagName tagName, String comment) throws TskCoreException {
+		if (artifact == null || tagName == null) {
+			throw new IllegalArgumentException("NULL argument passed to addArtifactTag");
+		}
+
+		CaseDbConnection connection = skCase.getConnection();
+		skCase.acquireSingleUserCaseWriteLock();
+		List<BlackboardArtifactTag> removedTags = new ArrayList<>();
+		List<String> removedTagIds = new ArrayList<>();
+		try {
+			connection.beginTransaction();
+			// If a TagName is part of a TagSet remove any existing tags from the
+			// set that are currenctly on the artifact
+			long tagSetId = tagName.getTagSetId();
+			if (tagSetId > 0) {
+				// Get the list of all of the blackboardArtifactTags that use
+				// TagName for the given artifact.
+				String selectQuery = String.format("SELECT * from blackboard_artifact_tags JOIN tag_names ON tag_names.tag_name_id = blackboard_artifact_tags.tag_name_id JOIN tsk_examiners on tsk_examiners.examiner_id = blackboard_artifact_tags.examiner_id WHERE artifact_id = %d AND tag_names.tag_set_id = %d", artifact.getArtifactID(), tagSetId);
+
+				try (Statement stmt = connection.createStatement(); ResultSet resultSet = stmt.executeQuery(selectQuery)) {
+					while (resultSet.next()) {
+						BlackboardArtifactTag bat
+								= new BlackboardArtifactTag(resultSet.getLong("tag_id"),
+										artifact,
+										skCase.getContentById(artifact.getObjectID()),
+										tagName,
+										resultSet.getString("comment"),
+										resultSet.getString("login_name"));
+
+						removedTags.add(bat);
+						removedTagIds.add(Long.toString(bat.getId()));
+					}
+				}
+
+				if (!removedTags.isEmpty()) {
+					// Remove the tags.
+					String removeQuery = String.format("DELETE FROM blackboard_artifact_tags WHERE tag_id IN (%s)", String.join(",", removedTagIds));
+					try (Statement stmt = connection.createStatement()) {
+						stmt.executeUpdate(removeQuery);
+					}
+				}
+			}
+
+			// Add the new Tag.
+			BlackboardArtifactTag artifactTag = null;
+			try (Statement stmt = connection.createStatement()) {
+				Examiner currentExaminer = skCase.getCurrentExaminer();
+				String query = String.format(
+						"INSERT INTO blackboard_artifact_tags (artifact_id, tag_name_id, comment, examiner_id) VALUES (%d, %d, '%s', %d)",
+						artifact.getArtifactID(),
+						tagName.getId(),
+						comment,
+						currentExaminer.getId());
+
+				if (skCase.getDatabaseType() == POSTGRESQL) {
+					stmt.execute(query, Statement.RETURN_GENERATED_KEYS);
+				} else {
+					stmt.execute(query);
+				}
+
+				try (ResultSet resultSet = stmt.getGeneratedKeys()) {
+					resultSet.next();
+					artifactTag = new BlackboardArtifactTag(resultSet.getLong(1), //last_insert_rowid()
+							artifact, skCase.getContentById(artifact.getObjectID()), tagName, comment, currentExaminer.getLoginName());
+				}
+			}
+
+			connection.commitTransaction();
+
+			return new BlackboardArtifactTagChange(artifactTag, removedTags);
+		} catch (SQLException ex) {
+			connection.rollbackTransaction();
+			throw new TskCoreException("Error adding row to blackboard_artifact_tags table (obj_id = " + artifact.getArtifactID() + ", tag_name_id = " + tagName.getId() + ")", ex);
+		} finally {
+
+			connection.close();
+			skCase.releaseSingleUserCaseWriteLock();
+		}
+	}
+
+	/**
+	 * Inserts a row into the content_tags table in the case database.
+	 *
+	 * @param content         The content to tag.
+	 * @param tagName         The name to use for the tag.
+	 * @param comment         A comment to store with the tag.
+	 * @param beginByteOffset Designates the beginning of a tagged section.
+	 * @param endByteOffset   Designates the end of a tagged section.
+	 *
+	 * @return A ContentTag data transfer object (DTO) for the new row.
+	 *
+	 * @throws TskCoreException
+	 */
+	public ContentTagChange addContentTag(Content content, TagName tagName, String comment, long beginByteOffset, long endByteOffset) throws TskCoreException {
+		CaseDbConnection connection = skCase.getConnection();
+		List<ContentTag> removedTags = new ArrayList<>();
+		List<String> removedTagIds = new ArrayList<>();
+		skCase.acquireSingleUserCaseWriteLock();
+		try {
+			connection.beginTransaction();
+			long tagSetId = tagName.getTagSetId();
+
+			if (tagSetId > 0) {
+				String selectQuery = String.format("SELECT * from content_tags JOIN tag_names ON tag_names.tag_name_id = content_tags.tag_name_id JOIN tsk_examiners on tsk_examiners.examiner_id = content_tags.examiner_id WHERE obj_id = %d AND tag_names.tag_set_id = %d", content.getId(), tagSetId);
+
+				try (Statement stmt = connection.createStatement(); ResultSet resultSet = stmt.executeQuery(selectQuery)) {
+					while (resultSet.next()) {
+						ContentTag bat
+								= new ContentTag(resultSet.getLong("tag_id"),
+										content,
+										tagName,
+										resultSet.getString("comment"),
+										resultSet.getLong("begin_byte_offset"),
+										resultSet.getLong("end_byte_offset"),
+										resultSet.getString("login_name"));
+
+						removedTagIds.add(Long.toString(bat.getId()));
+						removedTags.add(bat);
+					}
+				}
+
+				if (!removedTags.isEmpty()) {
+					String removeQuery = String.format("DELETE FROM content_tags WHERE tag_id IN (%s)", String.join(",", removedTagIds));
+					try (Statement stmt = connection.createStatement()) {
+						stmt.executeUpdate(removeQuery);
+					}
+				}
+			}
+
+			String queryTemplate = "INSERT INTO content_tags (obj_id, tag_name_id, comment, begin_byte_offset, end_byte_offset, examiner_id) VALUES (%d, %d, '%s', %d, %d, %d)";
+			ContentTag contentTag = null;
+			try (Statement stmt = connection.createStatement()) {
+				Examiner currentExaminer = skCase.getCurrentExaminer();
+				String query = String.format(queryTemplate,
+						content.getId(),
+						tagName.getId(),
+						comment,
+						beginByteOffset,
+						endByteOffset,
+						currentExaminer.getId());
+
+				if (skCase.getDatabaseType() == POSTGRESQL) {
+					stmt.executeUpdate(query, Statement.RETURN_GENERATED_KEYS);
+				} else {
+					stmt.executeUpdate(query);
+				}
+
+				try (ResultSet resultSet = stmt.getGeneratedKeys()) {
+					resultSet.next();
+					contentTag = new ContentTag(resultSet.getLong(1), //last_insert_rowid()
+							content, tagName, comment, beginByteOffset, endByteOffset, currentExaminer.getLoginName());
+				}
+			}
+
+			connection.commitTransaction();
+			return new ContentTagChange(contentTag, removedTags);
+		} catch (SQLException ex) {
+			connection.rollbackTransaction();
+			throw new TskCoreException("Error adding row to content_tags table (obj_id = " + content.getId() + ", tag_name_id = " + tagName.getId() + ")", ex);
+		} finally {
+			connection.close();
+			skCase.releaseSingleUserCaseWriteLock();
+		}
+	}
+
+	/**
+	 * Returns a list of all of the TagNames that are apart of the given TagSet.
+	 *
+	 * @param tagSetId ID of a TagSet.
+	 *
+	 * @return List of TagNames for the TagSet or empty list if none were found.
+	 *
+	 * @throws TskCoreException
+	 */
+	private List<TagName> getTagNamesByTagSetID(int tagSetId) throws TskCoreException {
+
+		if (tagSetId <= 0) {
+			throw new IllegalArgumentException("Invalid tagSetID passed to getTagNameByTagSetID");
+		}
+
+		List<TagName> tagNameList = new ArrayList<>();
+
+		CaseDbConnection connection = skCase.getConnection();
+
+		skCase.acquireSingleUserCaseReadLock();
+		String query = String.format("SELECT * FROM tag_names WHERE tag_set_id = %d", tagSetId);
+		try (Statement stmt = connection.createStatement(); ResultSet resultSet = stmt.executeQuery(query)) {
+			while (resultSet.next()) {
+				long tagId = resultSet.getLong("tag_name_id");
+				String tagName = resultSet.getString("display_name");
+				String description = resultSet.getString("description");
+				String color = resultSet.getString("color");
+				byte knownStatus = resultSet.getByte("knownStatus");
+
+				tagNameList.add(new TagName(tagId, tagName, description, TagName.HTML_COLOR.getColorByName(color), TskData.FileKnown.valueOf(knownStatus), tagSetId));
+			}
+		} catch (SQLException ex) {
+			throw new TskCoreException(String.format("Error getting tag names for tag set (%d)", tagSetId), ex);
+		} finally {
+			connection.close();
+			skCase.releaseSingleUserCaseReadLock();
+		}
+
+		return tagNameList;
+	}
+
+	/**
+	 * Object to store the tag change from a call to addArtifactTag.
+	 */
+	public static class BlackboardArtifactTagChange {
+
+		private final BlackboardArtifactTag addedTag;
+		private final List<BlackboardArtifactTag> removedTagList;
+
+		/**
+		 * Construct a new artifact tag change object.
+		 *
+		 * @param added		 Newly created artifact tag.
+		 * @param removed	List of removed tags.
+		 */
+		BlackboardArtifactTagChange(BlackboardArtifactTag added, List<BlackboardArtifactTag> removed) {
+			this.addedTag = added;
+			this.removedTagList = removed;
+		}
+
+		/**
+		 * Returns the newly created tag.
+		 *
+		 * @return Add artifact tag.
+		 */
+		public BlackboardArtifactTag getAddedTag() {
+			return addedTag;
+		}
+
+		/**
+		 * Returns a list of the artifacts tags that were removed.
+		 *
+		 * @return
+		 */
+		public List<BlackboardArtifactTag> getRemovedTags() {
+			return Collections.unmodifiableList(removedTagList);
+		}
+	}
+
+	/**
+	 * Object to store the tag change from a call to addContentTag.
+	 */
+	public static class ContentTagChange {
+
+		private final ContentTag addedTag;
+		private final List<ContentTag> removedTagList;
+
+		/**
+		 * Construct a new content tag change object.
+		 *
+		 * @param added		 Newly created artifact tag.
+		 * @param removed	List of removed tags.
+		 */
+		ContentTagChange(ContentTag added, List<ContentTag> removed) {
+			this.addedTag = added;
+			this.removedTagList = removed;
+		}
+
+		/**
+		 * Returns the newly created tag.
+		 *
+		 * @return Add artifact tag.
+		 */
+		public ContentTag getAddedTag() {
+			return addedTag;
+		}
+
+		/**
+		 * Returns a list of the artifacts tags that were removed.
+		 *
+		 * @return
+		 */
+		public List<ContentTag> getRemovedTags() {
+			return Collections.unmodifiableList(removedTagList);
+		}
+	}
+}
diff --git a/tsk/auto/db_sqlite.cpp b/tsk/auto/db_sqlite.cpp
index 2a0e8868f9ef1d5eee4e4726627b772c27ba3655..5625c5ad21207d7754d9616559b41930ce89d1f3 100644
--- a/tsk/auto/db_sqlite.cpp
+++ b/tsk/auto/db_sqlite.cpp
@@ -338,8 +338,11 @@ TskDbSqlite::initialize()
         ("CREATE TABLE tsk_files_derived_method (derived_id INTEGER PRIMARY KEY, tool_name TEXT NOT NULL, tool_version TEXT NOT NULL, other TEXT)",
             "Error creating tsk_files_derived_method table: %s\n")
         ||
+		attempt_exec
+		("CREATE TABLE tsk_tag_sets (tag_set_id INTEGER PRIMARY KEY, name TEXT UNIQUE)", "Error creating tsk_tag_sets table: %s\n")
+		||
         attempt_exec
-        ("CREATE TABLE tag_names (tag_name_id INTEGER PRIMARY KEY, display_name TEXT UNIQUE, description TEXT NOT NULL, color TEXT NOT NULL, knownStatus INTEGER NOT NULL)",
+        ("CREATE TABLE tag_names (tag_name_id INTEGER PRIMARY KEY, display_name TEXT UNIQUE, description TEXT NOT NULL, color TEXT NOT NULL, knownStatus INTEGER NOT NULL, tag_set_id INTEGER, FOREIGN KEY(tag_set_id) REFERENCES tsk_tag_sets(tag_set_id) ON DELETE SET NULL)",
             "Error creating tag_names table: %s\n")
         ||
         attempt_exec("CREATE TABLE review_statuses (review_status_id INTEGER PRIMARY KEY, "
diff --git a/tsk/auto/tsk_db.h b/tsk/auto/tsk_db.h
index 4497d30c8430bca6a9e302d5a484127168ee782c..096764acb30ad4047d5d4c860443b4e5c36ba16a 100755
--- a/tsk/auto/tsk_db.h
+++ b/tsk/auto/tsk_db.h
@@ -31,7 +31,7 @@ using std::string;
  * Keep these values in sync with CURRENT_DB_SCHEMA_VERSION in SleuthkitCase.java
  */
 #define TSK_SCHEMA_VER 8
-#define TSK_SCHEMA_MINOR_VER 4
+#define TSK_SCHEMA_MINOR_VER 5
 
 /**
  * Values for the type column in the tsk_objects table. 
diff --git a/tsk/fs/ext2fs.c b/tsk/fs/ext2fs.c
index 45dd18c277f241a06b10d5aca10a3d005431b127..1f56c943021b848a6047e11b73749e198b51957f 100755
--- a/tsk/fs/ext2fs.c
+++ b/tsk/fs/ext2fs.c
@@ -841,6 +841,20 @@ ext2fs_dinode_copy(EXT2FS_INFO * ext2fs, TSK_FS_META * fs_meta,
         grp_num * tsk_getu32(fs->endian,
         ext2fs->fs->s_inodes_per_group) + fs->first_inum;
 
+
+    /*
+     * Ensure that inum - ibase refers to a valid bit offset in imap_buf.
+     */
+    if ((inum - ibase) > fs->block_size*8) {
+        tsk_release_lock(&ext2fs->lock);
+        tsk_error_reset();
+        tsk_error_set_errno(TSK_ERR_FS_WALK_RNG);
+        tsk_error_set_errstr("ext2fs_dinode_copy: Invalid offset into imap_buf (inum %" PRIuINUM " - ibase %" PRIuINUM ")",
+            inum, ibase);
+        return 1;
+    }
+
+
     /*
      * Apply the allocated/unallocated restriction.
      */
@@ -1052,9 +1066,9 @@ ext2fs_inode_walk(TSK_FS_INFO * fs, TSK_INUM_T start_inum,
             ext2fs->fs->s_inodes_per_group) + 1;
 
         /*
-         * Ensure that inum - ibase refers to a valid offset in imap_buf.
+         * Ensure that inum - ibase refers to a valid bit offset in imap_buf.
          */
-        if ((inum - ibase) > fs->block_size) {
+        if ((inum - ibase) > fs->block_size*8) {
             tsk_release_lock(&ext2fs->lock);
             free(dino_buf);
             tsk_error_reset();