Package org.sleuthkit.datamodel

Source Code of org.sleuthkit.datamodel.SleuthkitCase$CaseDbTransaction

/*
* Sleuth Kit Data Model
*
* Copyright 2012-2014 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.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.text.MessageFormat;
import java.util.ResourceBundle;
import java.util.*;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.sleuthkit.datamodel.TskData.ObjectType;
import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE;
import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE;
import org.sleuthkit.datamodel.SleuthkitJNI.CaseDbHandle.AddImageProcess;
import org.sleuthkit.datamodel.TskData.FileKnown;
import org.sleuthkit.datamodel.TskData.TSK_DB_FILES_TYPE_ENUM;
import org.sleuthkit.datamodel.TskData.TSK_FS_META_FLAG_ENUM;
import org.sleuthkit.datamodel.TskData.TSK_FS_META_TYPE_ENUM;
import org.sleuthkit.datamodel.TskData.TSK_FS_NAME_FLAG_ENUM;
import org.sleuthkit.datamodel.TskData.TSK_FS_NAME_TYPE_ENUM;
import org.sqlite.SQLiteJDBCLoader;

/**
* Represents the case database with methods that provide abstractions for
* database operations.
*/
public class SleuthkitCase {

  private static final int SCHEMA_VERSION_NUMBER = 3; // This must be the same as TSK_SCHEMA_VER in tsk/auto/db_sqlite.cpp.       
  private static final int DATABASE_LOCKED_ERROR = 0; // This should be 6 according to documentation, but it has been observed to be 0.
  private static final int SQLITE_BUSY_ERROR = 5;
  private static final Logger logger = Logger.getLogger(SleuthkitCase.class.getName());
  private static final ResourceBundle bundle = ResourceBundle.getBundle("org.sleuthkit.datamodel.Bundle");
  private final ConnectionPerThreadDispenser connections = new ConnectionPerThreadDispenser();
  private final ResultSetHelper rsHelper = new ResultSetHelper(this);
  private final Map<Long, Long> carvedFileContainersCache = new HashMap<Long, Long>(); // Caches the IDs of the root $CarvedFiles for each volume.
  private final Map<Long, FileSystem> fileSystemIdMap = new HashMap<Long, FileSystem>(); // Cache for file system results.
  private final ArrayList<ErrorObserver> errorObservers = new ArrayList<ErrorObserver>();
  private final String dbPath;
  private final String dbDirPath;
  private SleuthkitJNI.CaseDbHandle caseHandle; // Not currently used.
  private int versionNumber;
  private String dbBackupPath;

  // This read/write lock is used to implement a layer of locking on top of
  // the locking protocol provided by the underlying SQLite database. The Java
  // locking protocol improves performance for reasons that are not currently
  // understood. Note that the lock is contructed to use a fairness policy.
  private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(true);

  /**
   * Private constructor, clients must use newCase() or openCase() method to
   * create an instance of this class.
   *
   * @param dbPath The full path to a SQLite case database file.
   * @param caseHandle A handle to a case database object in the native code
   * SleuthKit layer.
   * @throws Exception
   */
  private SleuthkitCase(String dbPath, SleuthkitJNI.CaseDbHandle caseHandle) throws Exception {
    Class.forName("org.sqlite.JDBC");
    this.dbPath = dbPath;
    this.dbDirPath = new java.io.File(dbPath).getParentFile().getAbsolutePath();
    this.caseHandle = caseHandle;
    initBlackboardArtifactTypes();
    initBlackboardAttributeTypes();
    updateDatabaseSchema();
    logSQLiteJDBCDriverInfo();
  }

  /**
   * Make sure the predefined artifact types are in the artifact types table.
   *
   * @throws SQLException
   */
  private void initBlackboardArtifactTypes() throws SQLException, TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    Statement statement = null;
    ResultSet resultSet = null;
    try {
      statement = connection.createStatement();
      for (ARTIFACT_TYPE type : ARTIFACT_TYPE.values()) {
        resultSet = connection.executeQuery(statement, "SELECT COUNT(*) from blackboard_artifact_types WHERE artifact_type_id = '" + type.getTypeID() + "'"); //NON-NLS
        if (resultSet.getLong(1) == 0) {
          connection.executeUpdate(statement, "INSERT INTO blackboard_artifact_types (artifact_type_id, type_name, display_name) VALUES (" + type.getTypeID() + " , '" + type.getLabel() + "', '" + type.getDisplayName() + "')"); //NON-NLS
        }
        resultSet.close();
        resultSet = null;
      }
    } finally {
      closeResultSet(resultSet);
      closeStatement(statement);
    }
  }

  /**
   * Make sure the predefined artifact attribute types are in the artifact
   * attribute types table.
   *
   * @throws SQLException
   */
  private void initBlackboardAttributeTypes() throws SQLException, TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    Statement statement = null;
    ResultSet resultSet = null;
    try {
      statement = connection.createStatement();
      for (ATTRIBUTE_TYPE type : ATTRIBUTE_TYPE.values()) {
        resultSet = connection.executeQuery(statement, "SELECT COUNT(*) from blackboard_attribute_types WHERE attribute_type_id = '" + type.getTypeID() + "'"); //NON-NLS
        if (resultSet.getLong(1) == 0) {
          connection.executeUpdate(statement, "INSERT INTO blackboard_attribute_types (attribute_type_id, type_name, display_name) VALUES (" + type.getTypeID() + ", '" + type.getLabel() + "', '" + type.getDisplayName() + "')"); //NON-NLS
        }
        resultSet.close();
        resultSet = null;
      }
    } finally {
      closeResultSet(resultSet);
      closeStatement(statement);
    }
  }

  /**
   * Modify the case database to bring it up-to-date with the current version
   * of the database schema.
   *
   * @throws Exception
   */
  private void updateDatabaseSchema() throws Exception {
    CaseDbConnection connection = connections.getConnection();
    ResultSet resultSet = null;
    Statement statement = null;
    try {
      connection.beginTransaction();

      // Get the schema version number of the case database from the tsk_db_info table.
      int schemaVersionNumber = SCHEMA_VERSION_NUMBER;
      statement = connection.createStatement();
      resultSet = connection.executeQuery(statement, "SELECT schema_ver FROM tsk_db_info"); //NON-NLS
      if (resultSet.next()) {
        schemaVersionNumber = resultSet.getInt("schema_ver"); //NON-NLS
      }
      resultSet.close();
      resultSet = null;

      // Do the schema update(s), if needed.
      if (SCHEMA_VERSION_NUMBER != schemaVersionNumber) {
        // Make a backup copy of the database. Client code can get the path of the backup
        // using the getBackupDatabasePath() method.
        String backupFilePath = dbPath + ".schemaVer" + schemaVersionNumber + ".backup"; //NON-NLS
        copyCaseDB(backupFilePath);
        dbBackupPath = backupFilePath;

        // ***CALL SCHEMA UPDATE METHODS HERE***
        // Each method should examine the schema number passed to it and either:
        //    a. do nothing and return the schema version number unchanged, or
        //    b. upgrade the database and then increment and return the schema version number.
        schemaVersionNumber = updateFromSchema2toSchema3(schemaVersionNumber);

        // Write the updated schema version number to the the tsk_db_info table.
        connection.executeUpdate(statement, "UPDATE tsk_db_info SET schema_ver = " + schemaVersionNumber); //NON-NLS
      }
      versionNumber = schemaVersionNumber;

      connection.commitTransaction();
    } catch (Exception ex) { // Cannot do exception multi-catch in Java 6, so use catch-all.
      connection.rollbackTransaction();
      throw ex;
    } finally {
      closeResultSet(resultSet);
      closeStatement(statement);
    }
  }

  /**
   * Make a duplicate / backup copy of the current case database. Makes a new
   * copy only, and continues to use the current connection.
   *
   * @param newDBPath Path to the copy to be created. File will be overwritten
   * if it exists.
   * @throws IOException if copying fails.
   */
  public void copyCaseDB(String newDBPath) throws IOException {
    InputStream in = null;
    OutputStream out = null;
    acquireExclusiveLock();
    try {
      InputStream inFile = new FileInputStream(dbPath);
      in = new BufferedInputStream(inFile);
      OutputStream outFile = new FileOutputStream(newDBPath);
      out = new BufferedOutputStream(outFile);
      int bytesRead = 0;
      while ((bytesRead = in.read()) != -1) {
        out.write(bytesRead);
      }
    } finally {
      try {
        if (in != null) {
          in.close();
        }
        if (out != null) {
          out.flush();
          out.close();
        }
      } catch (IOException e) {
        logger.log(Level.WARNING, "Could not close streams after db copy", e); //NON-NLS
      }
      releaseExclusiveLock();
    }
  }

  /**
   * Write some SQLite JDBC driver details to the log file.
   */
  private void logSQLiteJDBCDriverInfo() {
    try {
      SleuthkitCase.logger.info(String.format("sqlite-jdbc version %s loaded in %s mode", //NON-NLS
          SQLiteJDBCLoader.getVersion(), SQLiteJDBCLoader.isNativeMode()
          ? "native" : "pure-java")); //NON-NLS   
    } catch (Exception ex) {
      SleuthkitCase.logger.log(Level.SEVERE, "Error querying case database mode", ex);
    }
  }

  /**
   * Update a version 2 database schema to a version 3 database schema.
   *
   * @param schemaVersionNumber The schema version number of the database.
   * @return 3, if the input database schema version number was 2.
   * @throws SQLException
   * @throws TskCoreException
   */
  @SuppressWarnings("deprecation")
  private int updateFromSchema2toSchema3(int schemaVersionNumber) throws SQLException, TskCoreException {
    if (schemaVersionNumber != 2) {
      return schemaVersionNumber;
    }

    CaseDbConnection connection = connections.getConnection();
    Statement statement = null;
    Statement updateStatement = null;
    ResultSet resultSet = null;
    try {
      statement = connection.createStatement();

      // Add new tables for tags.
      statement.execute("CREATE TABLE tag_names (tag_name_id INTEGER PRIMARY KEY, display_name TEXT UNIQUE, description TEXT NOT NULL, color TEXT NOT NULL)"); //NON-NLS
      statement.execute("CREATE TABLE content_tags (tag_id INTEGER PRIMARY KEY, obj_id INTEGER NOT NULL, tag_name_id INTEGER NOT NULL, comment TEXT NOT NULL, begin_byte_offset INTEGER NOT NULL, end_byte_offset INTEGER NOT NULL)"); //NON-NLS
      statement.execute("CREATE TABLE blackboard_artifact_tags (tag_id INTEGER PRIMARY KEY, artifact_id INTEGER NOT NULL, tag_name_id INTEGER NOT NULL, comment TEXT NOT NULL)"); //NON-NLS

      // Add a new table for reports.
      statement.execute("CREATE TABLE reports (report_id INTEGER PRIMARY KEY, path TEXT NOT NULL, crtime INTEGER NOT NULL, src_module_name TEXT NOT NULL, report_name TEXT NOT NULL)"); //NON-NLS

      // Add new columns to the image info table.
      statement.execute("ALTER TABLE tsk_image_info ADD COLUMN size INTEGER;"); //NON-NLS
      statement.execute("ALTER TABLE tsk_image_info ADD COLUMN md5 TEXT;"); //NON-NLS
      statement.execute("ALTER TABLE tsk_image_info ADD COLUMN display_name TEXT;"); //NON-NLS

      // Add a new column to the file system info table.
      statement.execute("ALTER TABLE tsk_fs_info ADD COLUMN display_name TEXT;"); //NON-NLS

      // Add a new column to the file table.
      statement.execute("ALTER TABLE tsk_files ADD COLUMN meta_seq INTEGER;"); //NON-NLS

      // Add new columns and indexes to the attributes table and populate the
      // new column. Note that addition of the new column is a denormalization
      // to optimize attribute queries.
      statement.execute("ALTER TABLE blackboard_attributes ADD COLUMN artifact_type_id INTEGER NULL NOT NULL DEFAULT -1;"); //NON-NLS
      statement.execute("CREATE INDEX attribute_artifactTypeId ON blackboard_attributes(artifact_type_id);"); //NON-NLS
      statement.execute("CREATE INDEX attribute_valueText ON blackboard_attributes(value_text);"); //NON-NLS
      statement.execute("CREATE INDEX attribute_valueInt32 ON blackboard_attributes(value_int32);"); //NON-NLS
      statement.execute("CREATE INDEX attribute_valueInt64 ON blackboard_attributes(value_int64);"); //NON-NLS
      statement.execute("CREATE INDEX attribute_valueDouble ON blackboard_attributes(value_double);"); //NON-NLS
      resultSet = statement.executeQuery(
          "SELECT attrs.artifact_id, arts.artifact_type_id " + //NON-NLS
          "FROM blackboard_attributes AS attrs " + //NON-NLS
          "INNER JOIN blackboard_artifacts AS arts " + //NON-NLS
          "WHERE attrs.artifact_id = arts.artifact_id;"); //NON-NLS
      updateStatement = connection.createStatement();
      while (resultSet.next()) {
        long artifactId = resultSet.getLong(1);
        int artifactTypeId = resultSet.getInt(2);
        updateStatement.executeUpdate(
            "UPDATE blackboard_attributes " + //NON-NLS
            "SET artifact_type_id = " + artifactTypeId + " " + //NON-NLS
            "WHERE blackboard_attributes.artifact_id = " + artifactId + ";"); //NON-NLS         
      }
      resultSet.close();
      resultSet = null;

      // Convert existing tag artifact and attribute rows to rows in the new tags tables.
      // TODO: This code depends on prepared statements that could evolve with
      // time, breaking this upgrade. The code that follows should be rewritten
      // to do everything with SQL specific to case database schema version 2.
      HashMap<String, TagName> tagNames = new HashMap<String, TagName>();
      for (BlackboardArtifact artifact : getBlackboardArtifacts(ARTIFACT_TYPE.TSK_TAG_FILE)) {
        Content content = getContentById(artifact.getObjectID());
        String name = ""; //NON-NLS
        String comment = ""; //NON-NLS
        ArrayList<BlackboardAttribute> attributes = getBlackboardAttributes(artifact);
        for (BlackboardAttribute attribute : attributes) {
          if (attribute.getAttributeTypeID() == ATTRIBUTE_TYPE.TSK_TAG_NAME.getTypeID()) {
            name = attribute.getValueString();
          } else if (attribute.getAttributeTypeID() == ATTRIBUTE_TYPE.TSK_COMMENT.getTypeID()) {
            comment = attribute.getValueString();
          }
        }
        if (!name.isEmpty()) {
          TagName tagName;
          if (tagNames.containsKey(name)) {
            tagName = tagNames.get(name);
          } else {
            tagName = addTagName(name, "", TagName.HTML_COLOR.NONE); //NON-NLS
            tagNames.put(name, tagName);
          }
          addContentTag(content, tagName, comment, 0, content.getSize() - 1);
        }
      }
      for (BlackboardArtifact artifact : getBlackboardArtifacts(ARTIFACT_TYPE.TSK_TAG_ARTIFACT)) {
        long taggedArtifactId = -1;
        String name = ""; //NON-NLS
        String comment = ""; //NON-NLS
        ArrayList<BlackboardAttribute> attributes = getBlackboardAttributes(artifact);
        for (BlackboardAttribute attribute : attributes) {
          if (attribute.getAttributeTypeID() == ATTRIBUTE_TYPE.TSK_TAG_NAME.getTypeID()) {
            name = attribute.getValueString();
          } else if (attribute.getAttributeTypeID() == ATTRIBUTE_TYPE.TSK_COMMENT.getTypeID()) {
            comment = attribute.getValueString();
          } else if (attribute.getAttributeTypeID() == ATTRIBUTE_TYPE.TSK_TAGGED_ARTIFACT.getTypeID()) {
            taggedArtifactId = attribute.getValueLong();
          }
        }
        if (taggedArtifactId != -1 && !name.isEmpty()) {
          TagName tagName;
          if (tagNames.containsKey(name)) {
            tagName = tagNames.get(name);
          } else {
            tagName = addTagName(name, "", TagName.HTML_COLOR.NONE); //NON-NLS
            tagNames.put(name, tagName);
          }
          addBlackboardArtifactTag(getBlackboardArtifact(taggedArtifactId), tagName, comment);
        }
      }
      statement.execute(
          "DELETE FROM blackboard_attributes WHERE artifact_id IN " + //NON-NLS
          "(SELECT artifact_id FROM blackboard_artifacts WHERE artifact_type_id = " + ARTIFACT_TYPE.TSK_TAG_FILE.getTypeID() + //NON-NLS
          " OR artifact_type_id = " + ARTIFACT_TYPE.TSK_TAG_ARTIFACT.getTypeID() + ");"); //NON-NLS
      statement.execute(
          "DELETE FROM blackboard_artifacts WHERE " + //NON-NLS
          "artifact_type_id = " + ARTIFACT_TYPE.TSK_TAG_FILE.getTypeID() + //NON-NLS 
          " OR artifact_type_id = " + ARTIFACT_TYPE.TSK_TAG_ARTIFACT.getTypeID() + ";"); //NON-NLS

      return 3;
    } finally {
      closeStatement(updateStatement);
      closeResultSet(resultSet);
      closeStatement(statement);
    }
  }

  /**
   * Returns case database schema version number.
   *
   * @return The schema version number as an integer.
   */
  public int getSchemaVersion() {
    return this.versionNumber;
  }

  /**
   * Returns the path of a backup copy of the database made when a schema
   * version upgrade has occurred.
   *
   * @return The path of the backup file or null if no backup was made.
   */
  public String getBackupDatabasePath() {
    return dbBackupPath;
  }

  /**
   * Create a new transaction on the case database. The transaction object
   * that is returned can be passed to methods that take a CaseDbTransaction.
   * The caller is responsible for calling either commit() or rollback() on
   * the transaction object.
   *
   * @return A CaseDbTransaction object.
   * @throws TskCoreException
   */
  public CaseDbTransaction beginTransaction() throws TskCoreException {
    return new CaseDbTransaction(connections.getConnection());
  }

  /**
   * Get the full path to the case database directory.
   *
   * @return Absolute database directory path.
   */
  public String getDbDirPath() {
    return dbDirPath;
  }

  /**
   * Acquire the lock that provides exclusive access to the case database.
   * Call this method in a try block with a call to the lock release method in
   * an associated finally block.
   */
  public void acquireExclusiveLock() {
    rwLock.writeLock().lock();
  }

  /**
   * Release the lock that provides exclusive access to the database. This
   * method should always be called in the finally block of a try block in
   * which the lock was acquired.
   */
  public void releaseExclusiveLock() {
    rwLock.writeLock().unlock();
  }

  /**
   * Acquire the lock that provides shared access to the case database. Call
   * this method in a try block with a call to the lock release method in an
   * associated finally block.
   */
  public void acquireSharedLock() {
    rwLock.readLock().lock();
  }

  /**
   * Release the lock that provides shared access to the database. This method
   * should always be called in the finally block of a try block in which the
   * lock was acquired.
   */
  public void releaseSharedLock() {
    rwLock.readLock().unlock();
  }

  /**
   * Open an existing case database.
   *
   * @param dbPath Path to SQLite case database.
   * @return Case database object.
   * @throws org.sleuthkit.datamodel.TskCoreException
   */
  public static SleuthkitCase openCase(String dbPath) throws TskCoreException {
    final SleuthkitJNI.CaseDbHandle caseHandle = SleuthkitJNI.openCaseDb(dbPath);
    try {
      return new SleuthkitCase(dbPath, caseHandle);
    } catch (Exception ex) {
      throw new TskCoreException("Failed to open case database at " + dbPath, ex);
    }
  }

  /**
   * Create a new case database.
   *
   * @param dbPath Path to where SQlite case database should be created.
   * @return Case database object.
   * @throws org.sleuthkit.datamodel.TskCoreException
   */
  public static SleuthkitCase newCase(String dbPath) throws TskCoreException {
    SleuthkitJNI.CaseDbHandle caseHandle = SleuthkitJNI.newCaseDb(dbPath);
    try {
      return new SleuthkitCase(dbPath, caseHandle);
    } catch (Exception ex) {
      throw new TskCoreException("Failed to create case database at " + dbPath, ex);
    }
  }

  /**
   * Start process of adding a image to the case. Adding an image is a
   * multi-step process and this returns an object that allows it to happen.
   *
   * @param timezone TZ time zone string to use for ingest of image.
   * @param processUnallocSpace Set to true to process unallocated space in
   * the image.
   * @param noFatFsOrphans Set to true to skip processing orphan files of FAT
   * file systems.
   * @return Object that encapsulates control of adding an image via the
   * SleuthKit native code layer.
   */
  public AddImageProcess makeAddImageProcess(String timezone, boolean processUnallocSpace, boolean noFatFsOrphans) {
    return this.caseHandle.initAddImageProcess(timezone, processUnallocSpace, noFatFsOrphans);
  }

  /**
   * Get the list of root objects (data sources) from the case database, e.g.,
   * image files, logical (local) files, virtual directories.
   *
   * @return List of content objects representing root objects.
   * @throws TskCoreException
   */
  public List<Content> getRootObjects() throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT obj_id, type from tsk_objects " //NON-NLS
          + "WHERE par_obj_id IS NULL"); //NON-NLS     
      Collection<ObjectInfo> infos = new ArrayList<ObjectInfo>();
      while (rs.next()) {
        infos.add(new ObjectInfo(rs.getLong("obj_id"), ObjectType.valueOf(rs.getShort("type")))); //NON-NLS
      }

      List<Content> rootObjs = new ArrayList<Content>();
      for (ObjectInfo i : infos) {
        if (i.type == ObjectType.IMG) {
          rootObjs.add(getImageById(i.id));
        } else if (i.type == ObjectType.ABSTRACTFILE) {
          // Check if virtual dir for local files.
          AbstractFile af = getAbstractFileById(i.id);
          if (af instanceof VirtualDirectory) {
            rootObjs.add(af);
          } else {
            throw new TskCoreException("Parentless object has wrong type to be a root (ABSTRACTFILE, but not VIRTUAL_DIRECTORY: " + i.type);
          }
        } else {
          throw new TskCoreException("Parentless object has wrong type to be a root: " + i.type);
        }
      }
      return rootObjs;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting root objects", ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Get all blackboard artifacts of a given type.
   *
   * @param artifactTypeID artifact type id (must exist in database)
   * @return list of blackboard artifacts.
   * @throws TskCoreException
   */
  public ArrayList<BlackboardArtifact> getBlackboardArtifacts(int artifactTypeID) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet rs = null;
    try {
      String artifactTypeName = getArtifactTypeString(artifactTypeID);
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_ARTIFACTS_BY_TYPE);
      statement.clearParameters();
      statement.setInt(1, artifactTypeID);
      rs = connection.executeQuery(statement);
      ArrayList<BlackboardArtifact> artifacts = new ArrayList<BlackboardArtifact>();
      while (rs.next()) {
        artifacts.add(new BlackboardArtifact(this, rs.getLong(1), rs.getLong(2),
            artifactTypeID, artifactTypeName, ARTIFACT_TYPE.fromID(artifactTypeID).getDisplayName()));
      }
      return artifacts;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting or creating a blackboard artifact", ex);
    } finally {
      closeResultSet(rs);
      releaseSharedLock();
    }
  }

  /**
   * Get a count of blackboard artifacts for a given content.
   *
   * @param objId Id of the content.
   * @return The artifacts count for the content.
   * @throws TskCoreException
   */
  public long getBlackboardArtifactsCount(long objId) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet rs = null;
    try {
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.COUNT_ARTIFACTS_FROM_SOURCE);
      statement.clearParameters();
      statement.setLong(1, objId);
      rs = connection.executeQuery(statement);
      long count = 0;
      if (rs.next()) {
        count = rs.getLong(1);
      }
      return count;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting number of blackboard artifacts by content", ex);
    } finally {
      closeResultSet(rs);
      releaseSharedLock();
    }
  }

  /**
   * Get a count of artifacts of a given type.
   *
   * @param artifactTypeID Id of the artifact type.
   * @return The artifacts count for the type.
   * @throws TskCoreException
   */
  public long getBlackboardArtifactsTypeCount(int artifactTypeID) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet rs = null;
    try {
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.COUNT_ARTIFACTS_OF_TYPE);
      statement.clearParameters();
      statement.setInt(1, artifactTypeID);
      rs = connection.executeQuery(statement);
      long count = 0;
      if (rs.next()) {
        count = rs.getLong(1);
      }
      return count;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting number of blackboard artifacts by type", ex);
    } finally {
      closeResultSet(rs);
      releaseSharedLock();
    }
  }

  /**
   * Helper to iterate over blackboard artifacts result set containing all
   * columns and return a list of artifacts in the set. Must be enclosed in
   * acquireSharedLock. Result set and statement must be freed by the caller.
   *
   * @param rs existing, active result set (not closed by this method)
   * @return a list of blackboard artifacts in the result set
   * @throws SQLException if result set could not be iterated upon
   */
  private List<BlackboardArtifact> getArtifactsHelper(ResultSet rs) throws SQLException {
    ArrayList<BlackboardArtifact> artifacts = new ArrayList<BlackboardArtifact>();
    while (rs.next()) {
      final int artifactTypeID = rs.getInt(3);
      final ARTIFACT_TYPE artType = ARTIFACT_TYPE.fromID(artifactTypeID);
      artifacts.add(new BlackboardArtifact(this, rs.getLong(1), rs.getLong(2),
          artifactTypeID, artType.getLabel(), artType.getDisplayName()));
    }
    return artifacts;
  }

  /**
   * Get all blackboard artifacts that have an attribute of the given type and
   * String value
   *
   * @param attrType attribute of this attribute type to look for in the
   * artifacts
   * @param value value of the attribute of the attrType type to look for
   * @return a list of blackboard artifacts with such an attribute
   * @throws TskCoreException exception thrown if a critical error occurred
   * within tsk core and artifacts could not be queried
   */
  public List<BlackboardArtifact> getBlackboardArtifacts(BlackboardAttribute.ATTRIBUTE_TYPE attrType, String value) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT DISTINCT blackboard_artifacts.artifact_id, " //NON-NLS
          + "blackboard_artifacts.obj_id, blackboard_artifacts.artifact_type_id " //NON-NLS
          + "FROM blackboard_artifacts, blackboard_attributes " //NON-NLS
          + "WHERE blackboard_artifacts.artifact_id = blackboard_attributes.artifact_id " //NON-NLS
          + "AND blackboard_attributes.attribute_type_id IS " + attrType.getTypeID() //NON-NLS
          + " AND blackboard_attributes.value_text IS '" + value + "'");   //NON-NLS
      return getArtifactsHelper(rs);
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting blackboard artifacts by attribute", ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Get all blackboard artifacts that have an attribute of the given type and
   * String value
   *
   * @param attrType attribute of this attribute type to look for in the
   * artifacts
   * @param subString value substring of the string attribute of the attrType
   * type to look for
   * @param startsWith if true, the artifact attribute string should start
   * with the substring, if false, it should just contain it
   * @return a list of blackboard artifacts with such an attribute
   * @throws TskCoreException exception thrown if a critical error occurred
   * within tsk core and artifacts could not be queried
   */
  public List<BlackboardArtifact> getBlackboardArtifacts(BlackboardAttribute.ATTRIBUTE_TYPE attrType, String subString, boolean startsWith) throws TskCoreException {
    subString = "%" + subString; //NON-NLS
    if (startsWith == false) {
      subString = subString + "%"; //NON-NLS
    }
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT DISTINCT blackboard_artifacts.artifact_id, " //NON-NLS
          + "blackboard_artifacts.obj_id, blackboard_artifacts.artifact_type_id " //NON-NLS
          + "FROM blackboard_artifacts, blackboard_attributes " //NON-NLS
          + "WHERE blackboard_artifacts.artifact_id = blackboard_attributes.artifact_id " //NON-NLS
          + "AND blackboard_attributes.attribute_type_id IS " + attrType.getTypeID() //NON-NLS
          + " AND blackboard_attributes.value_text LIKE '" + subString + "'"); //NON-NLS     
      return getArtifactsHelper(rs);
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting blackboard artifacts by attribute. " + ex.getMessage(), ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Get all blackboard artifacts that have an attribute of the given type and
   * integer value
   *
   * @param attrType attribute of this attribute type to look for in the
   * artifacts
   * @param value value of the attribute of the attrType type to look for
   * @return a list of blackboard artifacts with such an attribute
   * @throws TskCoreException exception thrown if a critical error occurred
   * within tsk core and artifacts could not be queried
   */
  public List<BlackboardArtifact> getBlackboardArtifacts(BlackboardAttribute.ATTRIBUTE_TYPE attrType, int value) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT DISTINCT blackboard_artifacts.artifact_id, " //NON-NLS
          + "blackboard_artifacts.obj_id, blackboard_artifacts.artifact_type_id " //NON-NLS
          + "FROM blackboard_artifacts, blackboard_attributes " //NON-NLS
          + "WHERE blackboard_artifacts.artifact_id = blackboard_attributes.artifact_id " //NON-NLS
          + "AND blackboard_attributes.attribute_type_id IS " + attrType.getTypeID() //NON-NLS
          + " AND blackboard_attributes.value_int32 IS " + value); //NON-NLS
      return getArtifactsHelper(rs);
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting blackboard artifacts by attribute", ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Get all blackboard artifacts that have an attribute of the given type and
   * long value
   *
   * @param attrType attribute of this attribute type to look for in the
   * artifacts
   * @param value value of the attribute of the attrType type to look for
   * @return a list of blackboard artifacts with such an attribute
   * @throws TskCoreException exception thrown if a critical error occurred
   * within tsk core and artifacts could not be queried
   */
  public List<BlackboardArtifact> getBlackboardArtifacts(BlackboardAttribute.ATTRIBUTE_TYPE attrType, long value) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT DISTINCT blackboard_artifacts.artifact_id, " //NON-NLS
          + "blackboard_artifacts.obj_id, blackboard_artifacts.artifact_type_id " //NON-NLS
          + "FROM blackboard_artifacts, blackboard_attributes " //NON-NLS
          + "WHERE blackboard_artifacts.artifact_id = blackboard_attributes.artifact_id " //NON-NLS
          + "AND blackboard_attributes.attribute_type_id IS " + attrType.getTypeID() //NON-NLS
          + " AND blackboard_attributes.value_int64 IS " + value); //NON-NLS     
      return getArtifactsHelper(rs);
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting blackboard artifacts by attribute. " + ex.getMessage(), ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Get all blackboard artifacts that have an attribute of the given type and
   * double value
   *
   * @param attrType attribute of this attribute type to look for in the
   * artifacts
   * @param value value of the attribute of the attrType type to look for
   * @return a list of blackboard artifacts with such an attribute
   * @throws TskCoreException exception thrown if a critical error occurred
   * within tsk core and artifacts could not be queried
   */
  public List<BlackboardArtifact> getBlackboardArtifacts(BlackboardAttribute.ATTRIBUTE_TYPE attrType, double value) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT DISTINCT blackboard_artifacts.artifact_id, " //NON-NLS
          + "blackboard_artifacts.obj_id, blackboard_artifacts.artifact_type_id " //NON-NLS
          + "FROM blackboard_artifacts, blackboard_attributes " //NON-NLS
          + "WHERE blackboard_artifacts.artifact_id = blackboard_attributes.artifact_id " //NON-NLS
          + "AND blackboard_attributes.attribute_type_id IS " + attrType.getTypeID() //NON-NLS
          + " AND blackboard_attributes.value_double IS " + value); //NON-NLS
      return getArtifactsHelper(rs);
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting blackboard artifacts by attribute", ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Get all blackboard artifacts that have an attribute of the given type and
   * byte value
   *
   * @param attrType attribute of this attribute type to look for in the
   * artifacts
   * @param value value of the attribute of the attrType type to look for
   * @return a list of blackboard artifacts with such an attribute
   * @throws TskCoreException exception thrown if a critical error occurred
   * within tsk core and artifacts could not be queried
   */
  public List<BlackboardArtifact> getBlackboardArtifacts(BlackboardAttribute.ATTRIBUTE_TYPE attrType, byte value) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT DISTINCT blackboard_artifacts.artifact_id, " //NON-NLS
          + "blackboard_artifacts.obj_id, blackboard_artifacts.artifact_type_id " //NON-NLS
          + "FROM blackboard_artifacts, blackboard_attributes " //NON-NLS
          + "WHERE blackboard_artifacts.artifact_id = blackboard_attributes.artifact_id " //NON-NLS
          + "AND blackboard_attributes.attribute_type_id IS " + attrType.getTypeID() //NON-NLS
          + " AND blackboard_attributes.value_byte IS " + value); //NON-NLS
      return getArtifactsHelper(rs);
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting blackboard artifacts by attribute", ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Get _standard_ blackboard artifact types in use. This does not currently
   * return user-defined ones.
   *
   * @return list of blackboard artifact types
   * @throws TskCoreException exception thrown if a critical error occurred
   * within tsk core
   */
  public ArrayList<BlackboardArtifact.ARTIFACT_TYPE> getBlackboardArtifactTypes() throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT artifact_type_id FROM blackboard_artifact_types"); //NON-NLS     
      ArrayList<BlackboardArtifact.ARTIFACT_TYPE> artifact_types = new ArrayList<BlackboardArtifact.ARTIFACT_TYPE>();
      while (rs.next()) {
        /*
         * Only return ones in the enum because otherwise exceptions
         * get thrown down the call stack. Need to remove use of enum
         * for the attribute types */
        for (BlackboardArtifact.ARTIFACT_TYPE artType : BlackboardArtifact.ARTIFACT_TYPE.values()) {
          if (artType.getTypeID() == rs.getInt(1)) {
            artifact_types.add(artType);
          }
        }
      }
      return artifact_types;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting artifact types", ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Get all of the blackboard artifact types that are in use in the
   * blackboard.
   *
   * @return List of blackboard artifact types
   * @throws TskCoreException
   */
  public ArrayList<BlackboardArtifact.ARTIFACT_TYPE> getBlackboardArtifactTypesInUse() throws TskCoreException {
    // @@@ TODO: This should be rewritten as a single query.    
    ArrayList<BlackboardArtifact.ARTIFACT_TYPE> allArts = getBlackboardArtifactTypes();
    ArrayList<BlackboardArtifact.ARTIFACT_TYPE> usedArts = new ArrayList<BlackboardArtifact.ARTIFACT_TYPE>();
    for (BlackboardArtifact.ARTIFACT_TYPE art : allArts) {
      if (getBlackboardArtifactsTypeCount(art.getTypeID()) > 0) {
        usedArts.add(art);
      }
    }
    return usedArts;
  }

  /**
   * Get all blackboard attribute types
   *
   * Gets both static (in enum) and dynamic attributes types (created by
   * modules at runtime)
   *
   * @return list of blackboard attribute types
   * @throws TskCoreException exception thrown if a critical error occurred
   * within tsk core
   */
  public ArrayList<BlackboardAttribute.ATTRIBUTE_TYPE> getBlackboardAttributeTypes() throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT type_name FROM blackboard_attribute_types"); //NON-NLS
      ArrayList<BlackboardAttribute.ATTRIBUTE_TYPE> attribute_types = new ArrayList<BlackboardAttribute.ATTRIBUTE_TYPE>();
      while (rs.next()) {
        attribute_types.add(BlackboardAttribute.ATTRIBUTE_TYPE.fromLabel(rs.getString(1)));
      }
      return attribute_types;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting attribute types", ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Get count of blackboard attribute types
   *
   * Counts both static (in enum) and dynamic attributes types (created by
   * modules at runtime)
   *
   * @return count of attribute types
   * @throws TskCoreException exception thrown if a critical error occurs
   * within TSK core
   */
  public int getBlackboardAttributeTypesCount() throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT COUNT(*) FROM blackboard_attribute_types"); //NON-NLS
      int count = 0;
      if (rs.next()) {
        count = rs.getInt(1);
      }
      return count;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting number of blackboard artifacts by type", ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Helper method to get all artifacts matching the type id name and object
   * id
   *
   * @param artifactTypeID artifact type id
   * @param artifactTypeName artifact type name
   * @param obj_id associated object id
   * @return list of blackboard artifacts
   * @throws TskCoreException exception thrown if a critical error occurs
   * within TSK core
   */
  private ArrayList<BlackboardArtifact> getArtifactsHelper(int artifactTypeID, String artifactTypeName, long obj_id) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet rs = null;
    try {
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_ARTIFACTS_BY_SOURCE_AND_TYPE);
      statement.clearParameters();
      statement.setLong(1, obj_id);
      statement.setInt(2, artifactTypeID);
      rs = connection.executeQuery(statement);
      ArrayList<BlackboardArtifact> artifacts = new ArrayList<BlackboardArtifact>();
      while (rs.next()) {
        artifacts.add(new BlackboardArtifact(this, rs.getLong(1), obj_id, artifactTypeID, artifactTypeName, this.getArtifactTypeDisplayName(artifactTypeID)));
      }
      return artifacts;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting or creating a blackboard artifact", ex);
    } finally {
      closeResultSet(rs);
      releaseSharedLock();
    }
  }

  /**
   * Helper method to get count of all artifacts matching the type id name and
   * object id
   *
   * @param artifactTypeID artifact type id
   * @param obj_id associated object id
   * @return count of matching blackboard artifacts
   * @throws TskCoreException exception thrown if a critical error occurs
   * within TSK core
   */
  private long getArtifactsCountHelper(int artifactTypeID, long obj_id) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet rs = null;
    try {
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.COUNT_ARTIFACTS_BY_SOURCE_AND_TYPE);
      statement.clearParameters();
      statement.setLong(1, obj_id);
      statement.setInt(2, artifactTypeID);
      rs = connection.executeQuery(statement);
      long count = 0;
      if (rs.next()) {
        count = rs.getLong(1);
      }
      return count;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting blackboard artifact count", ex);
    } finally {
      closeResultSet(rs);
      releaseSharedLock();
    }
  }

  /**
   * Helper method to get all artifacts matching the type id name.
   *
   * @param artifactTypeID artifact type id
   * @param artifactTypeName artifact type name
   * @return list of blackboard artifacts
   * @throws TskCoreException exception thrown if a critical error occurs
   * within TSK core
   */
  private ArrayList<BlackboardArtifact> getArtifactsHelper(int artifactTypeID, String artifactTypeName) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet rs = null;
    try {
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_ARTIFACTS_BY_TYPE);
      statement.clearParameters();
      statement.setInt(1, artifactTypeID);
      rs = connection.executeQuery(statement);
      ArrayList<BlackboardArtifact> artifacts = new ArrayList<BlackboardArtifact>();
      while (rs.next()) {
        artifacts.add(new BlackboardArtifact(this, rs.getLong(1), rs.getLong(2), artifactTypeID, artifactTypeName, this.getArtifactTypeDisplayName(artifactTypeID)));
      }
      return artifacts;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting or creating a blackboard artifact", ex);
    } finally {
      closeResultSet(rs);
      releaseSharedLock();
    }
  }

  /**
   * Get all blackboard artifacts of a given type for the given object id
   *
   * @param artifactTypeName artifact type name
   * @param obj_id object id
   * @return list of blackboard artifacts
   * @throws TskCoreException exception thrown if a critical error occurs
   * within TSK core
   */
  public ArrayList<BlackboardArtifact> getBlackboardArtifacts(String artifactTypeName, long obj_id) throws TskCoreException {
    int artifactTypeID = this.getArtifactTypeID(artifactTypeName);
    if (artifactTypeID == -1) {
      return new ArrayList<BlackboardArtifact>();
    }
    return getArtifactsHelper(artifactTypeID, artifactTypeName, obj_id);
  }

  /**
   * Get all blackboard artifacts of a given type for the given object id
   *
   * @param artifactTypeID artifact type id (must exist in database)
   * @param obj_id object id
   * @return list of blackboard artifacts
   * @throws TskCoreException exception thrown if a critical error occurs
   * within TSK core
   */
  public ArrayList<BlackboardArtifact> getBlackboardArtifacts(int artifactTypeID, long obj_id) throws TskCoreException {
    String artifactTypeName = this.getArtifactTypeString(artifactTypeID);
    return getArtifactsHelper(artifactTypeID, artifactTypeName, obj_id);
  }

  /**
   * Get all blackboard artifacts of a given type for the given object id
   *
   * @param artifactType artifact type enum
   * @param obj_id object id
   * @return list of blackboard artifacts
   * @throws TskCoreException exception thrown if a critical error occurs
   * within TSK core
   */
  public ArrayList<BlackboardArtifact> getBlackboardArtifacts(ARTIFACT_TYPE artifactType, long obj_id) throws TskCoreException {
    return getArtifactsHelper(artifactType.getTypeID(), artifactType.getLabel(), obj_id);
  }

  /**
   * Get count of all blackboard artifacts of a given type for the given
   * object id
   *
   * @param artifactTypeName artifact type name
   * @param obj_id object id
   * @return count of blackboard artifacts
   * @throws TskCoreException exception thrown if a critical error occurs
   * within TSK core
   */
  public long getBlackboardArtifactsCount(String artifactTypeName, long obj_id) throws TskCoreException {
    int artifactTypeID = this.getArtifactTypeID(artifactTypeName);
    if (artifactTypeID == -1) {
      return 0;
    }
    return getArtifactsCountHelper(artifactTypeID, obj_id);
  }

  /**
   * Get count of all blackboard artifacts of a given type for the given
   * object id
   *
   * @param artifactTypeID artifact type id (must exist in database)
   * @param obj_id object id
   * @return count of blackboard artifacts
   * @throws TskCoreException exception thrown if a critical error occurs
   * within TSK core
   */
  public long getBlackboardArtifactsCount(int artifactTypeID, long obj_id) throws TskCoreException {
    return getArtifactsCountHelper(artifactTypeID, obj_id);
  }

  /**
   * Get count of all blackboard artifacts of a given type for the given
   * object id
   *
   * @param artifactType artifact type enum
   * @param obj_id object id
   * @return count of blackboard artifacts
   * @throws TskCoreException exception thrown if a critical error occurs
   * within TSK core
   */
  public long getBlackboardArtifactsCount(ARTIFACT_TYPE artifactType, long obj_id) throws TskCoreException {
    return getArtifactsCountHelper(artifactType.getTypeID(), obj_id);
  }

  /**
   * Get all blackboard artifacts of a given type
   *
   * @param artifactTypeName artifact type name
   * @return list of blackboard artifacts
   * @throws TskCoreException exception thrown if a critical error occurs
   * within TSK core
   */
  public ArrayList<BlackboardArtifact> getBlackboardArtifacts(String artifactTypeName) throws TskCoreException {
    int artifactTypeID = this.getArtifactTypeID(artifactTypeName);
    if (artifactTypeID == -1) {
      return new ArrayList<BlackboardArtifact>();
    }
    return getArtifactsHelper(artifactTypeID, artifactTypeName);
  }

  /**
   * Get all blackboard artifacts of a given type
   *
   * @param artifactType artifact type enum
   * @return list of blackboard artifacts
   * @throws TskCoreException exception thrown if a critical error occurs
   * within TSK core
   */
  public ArrayList<BlackboardArtifact> getBlackboardArtifacts(ARTIFACT_TYPE artifactType) throws TskCoreException {
    return getArtifactsHelper(artifactType.getTypeID(), artifactType.getLabel());
  }

  /**
   * Get all blackboard artifacts of a given type with an attribute of a given
   * type and String value.
   *
   * @param artifactType artifact type enum
   * @param attrType attribute type enum
   * @param value String value of attribute
   * @return list of blackboard artifacts
   * @throws TskCoreException exception thrown if a critical error occurs
   * within TSK core
   */
  public List<BlackboardArtifact> getBlackboardArtifacts(ARTIFACT_TYPE artifactType, BlackboardAttribute.ATTRIBUTE_TYPE attrType, String value) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT DISTINCT blackboard_artifacts.artifact_id, " //NON-NLS
          + "blackboard_artifacts.obj_id, blackboard_artifacts.artifact_type_id " //NON-NLS
          + "FROM blackboard_artifacts, blackboard_attributes " //NON-NLS
          + "WHERE blackboard_artifacts.artifact_id = blackboard_attributes.artifact_id " //NON-NLS
          + "AND blackboard_attributes.attribute_type_id IS " + attrType.getTypeID() //NON-NLS
          + " AND blackboard_artifacts.artifact_type_id = " + artifactType.getTypeID() //NON-NLS
          + " AND blackboard_attributes.value_text IS '" + value + "'"); //NON-NLS
      return getArtifactsHelper(rs);
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting blackboard artifacts by artifact type and attribute. " + ex.getMessage(), ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Get the blackboard artifact with the given artifact id
   *
   * @param artifactID artifact ID
   * @return blackboard artifact
   * @throws TskCoreException exception thrown if a critical error occurs
   * within TSK core
   */
  public BlackboardArtifact getBlackboardArtifact(long artifactID) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet rs = null;
    try {
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_ARTIFACT_BY_ID);
      statement.clearParameters();
      statement.setLong(1, artifactID);
      rs = connection.executeQuery(statement);
      long obj_id = rs.getLong(1);
      int artifact_type_id = rs.getInt(2);
      return new BlackboardArtifact(this, artifactID, obj_id, artifact_type_id,
          this.getArtifactTypeString(artifact_type_id), this.getArtifactTypeDisplayName(artifact_type_id));
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting a blackboard artifact. " + ex.getMessage(), ex);
    } finally {
      closeResultSet(rs);
      releaseSharedLock();
    }
  }

  /**
   * Add a blackboard attribute.
   *
   * @param attr A blackboard attribute.
   * @param artifactTypeId The type of artifact associated with the attribute.
   * @throws TskCoreException thrown if a critical error occurs.
   */
  public void addBlackboardAttribute(BlackboardAttribute attr, int artifactTypeId) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireExclusiveLock();
    try {
      addBlackBoardAttribute(attr, artifactTypeId, connection);
    } catch (SQLException ex) {
      throw new TskCoreException("Error adding blackboard attribute " + attr.toString(), ex);
    } finally {
      releaseExclusiveLock();
    }
  }

  /**
   * Add a set blackboard attributes.
   *
   * @param attributes A set of blackboard attribute.
   * @param artifactTypeId The type of artifact associated with the
   * attributes.
   * @throws TskCoreException thrown if a critical error occurs.
   */
  public void addBlackboardAttributes(Collection<BlackboardAttribute> attributes, int artifactTypeId) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireExclusiveLock();
    try {
      connection.beginTransaction();
      for (final BlackboardAttribute attr : attributes) {
        addBlackBoardAttribute(attr, artifactTypeId, connection);
      }
      connection.commitTransaction();
    } catch (SQLException ex) {
      connection.rollbackTransaction();
      throw new TskCoreException("Error adding blackboard attributes", ex);
    } finally {
      releaseExclusiveLock();
    }
  }

  private void addBlackBoardAttribute(BlackboardAttribute attr, int artifactTypeId, CaseDbConnection connection) throws SQLException, TskCoreException {
    PreparedStatement statement;
    switch (attr.getValueType()) {
      case STRING:
        statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.INSERT_STRING_ATTRIBUTE);
        statement.clearParameters();
        statement.setString(7, escapeForBlackboard(attr.getValueString()));
        break;
      case BYTE:
        statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.INSERT_BYTE_ATTRIBUTE);
        statement.clearParameters();
        statement.setBytes(7, attr.getValueBytes());
        break;
      case INTEGER:
        statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.INSERT_INT_ATTRIBUTE);
        statement.clearParameters();
        statement.setInt(7, attr.getValueInt());
        break;
      case LONG:
        statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.INSERT_LONG_ATTRIBUTE);
        statement.clearParameters();
        statement.setLong(7, attr.getValueLong());
        break;
      case DOUBLE:
        statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.INSERT_DOUBLE_ATTRIBUTE);
        statement.clearParameters();
        statement.setDouble(7, attr.getValueDouble());
        break;
      default:
        throw new TskCoreException("Unrecognized artifact attribute value type");
    }
    statement.setLong(1, attr.getArtifactID());
    statement.setInt(2, artifactTypeId);
    statement.setString(3, attr.getModuleName());
    statement.setString(4, attr.getContext());
    statement.setInt(5, attr.getAttributeTypeID());
    statement.setLong(6, attr.getValueType().getType());
    connection.executeUpdate(statement);
  }

  /**
   * add an attribute type with the given name
   *
   * @param attrTypeString name of the new attribute
   * @param displayName the (non-unique) display name of the attribute type
   * @return the id of the new attribute
   * @throws TskCoreException exception thrown if a critical error occurs
   * within tsk core
   */
  public int addAttrType(String attrTypeString, String displayName) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireExclusiveLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      connection.beginTransaction();
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT attribute_type_id FROM blackboard_attribute_types WHERE type_name = '" + attrTypeString + "'"); //NON-NLS
      if (!rs.next()) {
        rs.close();
        connection.executeUpdate(s, "INSERT INTO blackboard_artifact_types (type_name, display_name) VALUES (" + attrTypeString + "', '" + displayName + "')"); //NON-NLS
        rs = s.getGeneratedKeys();
      }
      int type = rs.getInt(1);
      connection.commitTransaction();
      return type;
    } catch (SQLException ex) {
      connection.rollbackTransaction();
      throw new TskCoreException("Error adding attribute type", ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseExclusiveLock();
    }
  }

  /**
   * Get the attribute type id associated with an attribute type name.
   *
   * @param attrTypeName An attribute type name.
   * @return An attribute id or -1 if the attribute type does not exist.
   * @throws TskCoreException If an error occurs accessing the case database.
   *
   */
  public int getAttrTypeID(String attrTypeName) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT attribute_type_id FROM blackboard_attribute_types WHERE type_name = '" + attrTypeName + "'"); //NON-NLS
      int typeId = -1;
      if (rs.next()) {
        typeId = rs.getInt(1);
      }
      return typeId;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting attribute type id", ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Get the string associated with the given id. Will throw an error if that
   * id does not exist
   *
   * @param attrTypeID attribute id
   * @return string associated with the given id
   * @throws TskCoreException exception thrown if a critical error occurs
   * within tsk core
   */
  public String getAttrTypeString(int attrTypeID) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT type_name FROM blackboard_attribute_types WHERE attribute_type_id = " + attrTypeID); //NON-NLS
      if (rs.next()) {
        return rs.getString(1);
      } else {
        throw new TskCoreException("No type with that id");
      }
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting or creating a attribute type name", ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Get the display name for the attribute with the given id. Will throw an
   * error if that id does not exist
   *
   * @param attrTypeID attribute id
   * @return string associated with the given id
   * @throws TskCoreException exception thrown if a critical error occurs
   * within tsk core
   */
  public String getAttrTypeDisplayName(int attrTypeID) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT display_name FROM blackboard_attribute_types WHERE attribute_type_id = " + attrTypeID); //NON-NLS
      if (rs.next()) {
        return rs.getString(1);
      } else {
        throw new TskCoreException("No type with that id");
      }
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting or creating a attribute type name", ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Get the artifact type id associated with an artifact type name.
   *
   * @param artifactTypeName An artifact type name.
   * @return An artifact id or -1 if the attribute type does not exist.
   * @throws TskCoreException If an error occurs accessing the case database.
   *
   */
  public int getArtifactTypeID(String artifactTypeName) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT artifact_type_id FROM blackboard_artifact_types WHERE type_name = '" + artifactTypeName + "'"); //NON-NLS
      int typeId = -1;
      if (rs.next()) {
        typeId = rs.getInt(1);
      }
      return typeId;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting artifact type id", ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Get artifact type name for the given string. Will throw an error if that
   * artifact doesn't exist. Use addArtifactType(...) to create a new one.
   *
   * @param artifactTypeID id for an artifact type
   * @return name of that artifact type
   * @throws TskCoreException exception thrown if a critical error occurs
   * within tsk core
   */
  String getArtifactTypeString(int artifactTypeID) throws TskCoreException {
    // TODO: This should return null, not throw an exception
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT type_name FROM blackboard_artifact_types WHERE artifact_type_id = " + artifactTypeID); //NON-NLS
      if (rs.next()) {
        return rs.getString(1);
      } else {
        throw new TskCoreException("Error getting artifact type name, artifact type id = " + artifactTypeID + " not found");
      }
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting artifact type name, artifact type id = " + artifactTypeID, ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Get artifact type display name for the given string. Will throw an error
   * if that artifact doesn't exist. Use addArtifactType(...) to create a new
   * one.
   *
   * @param artifactTypeID id for an artifact type
   * @return display name of that artifact type
   * @throws TskCoreException exception thrown if a critical error occurs
   * within tsk core
   */
  String getArtifactTypeDisplayName(int artifactTypeID) throws TskCoreException {
    // TODO: This should return null, not throw an exception
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT display_name FROM blackboard_artifact_types WHERE artifact_type_id = " + artifactTypeID); //NON-NLS
      if (rs.next()) {
        return rs.getString(1);
      } else {
        throw new TskCoreException("Error getting artifact type display name, artifact type id = " + artifactTypeID + " not found");
      }
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting artifact type display name, artifact type id = " + artifactTypeID, ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Add an artifact type with the given name. Will return an id that can be
   * used to look that artifact type up.
   *
   * @param artifactTypeName System (unique) name of artifact
   * @param displayName Display (non-unique) name of artifact
   * @return ID of artifact added
   * @throws TskCoreException exception thrown if a critical error occurs
   * within tsk core
   */
  public int addArtifactType(String artifactTypeName, String displayName) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireExclusiveLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      connection.beginTransaction();
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT artifact_type_id FROM blackboard_artifact_types WHERE type_name = '" + artifactTypeName + "'"); //NON-NLS
      if (!rs.next()) {
        rs.close();
        connection.executeUpdate(s, "INSERT INTO blackboard_artifact_types (type_name, display_name) VALUES (" + artifactTypeName + "', '" + displayName + "')"); //NON-NLS
        rs = s.getGeneratedKeys();
      }
      int id = rs.getInt(1);
      connection.commitTransaction();
      return id;
    } catch (SQLException ex) {
      connection.rollbackTransaction();
      throw new TskCoreException("Error adding artifact type", ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseExclusiveLock();
    }
  }

  public ArrayList<BlackboardAttribute> getBlackboardAttributes(final BlackboardArtifact artifact) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet rs = null;
    try {
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_ATTRIBUTES_OF_ARTIFACT);
      statement.clearParameters();
      statement.setLong(1, artifact.getArtifactID());
      rs = connection.executeQuery(statement);
      ArrayList<BlackboardAttribute> attributes = new ArrayList<BlackboardAttribute>();
      while (rs.next()) {
        final BlackboardAttribute attr = new BlackboardAttribute(
            rs.getLong(1),
            rs.getInt(4),
            rs.getString(2),
            rs.getString(3),
            BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.fromType(rs.getInt(5)),
            rs.getInt(8),
            rs.getLong(9),
            rs.getDouble(10),
            rs.getString(7),
            rs.getBytes(6), this);
        attributes.add(attr);
      }
      return attributes;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting attributes for artifact, artifact id = " + artifact.getArtifactID(), ex);
    } finally {
      closeResultSet(rs);
      releaseSharedLock();
    }
  }

  /**
   * Get all attributes that match a where clause. The clause should begin
   * with "WHERE" or "JOIN". To use this method you must know the database
   * tables
   *
   * @param whereClause a sqlite where clause
   * @return a list of matching attributes
   * @throws TskCoreException exception thrown if a critical error occurs
   * within tsk core
   */
  public ArrayList<BlackboardAttribute> getMatchingAttributes(String whereClause) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "Select artifact_id, source, context, attribute_type_id, value_type, " //NON-NLS
          + "value_byte, value_text, value_int32, value_int64, value_double FROM blackboard_attributes " + whereClause); //NON-NLS
      ArrayList<BlackboardAttribute> matches = new ArrayList<BlackboardAttribute>();
      while (rs.next()) {
        BlackboardAttribute attr = new BlackboardAttribute(rs.getLong("artifact_id"), rs.getInt("attribute_type_id"), rs.getString("source"), rs.getString("context"), //NON-NLS
            BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.fromType(rs.getInt("value_type")), rs.getInt("value_int32"), rs.getLong("value_int64"), rs.getDouble("value_double"), //NON-NLS
            rs.getString("value_text"), rs.getBytes("value_byte"), this); //NON-NLS
        matches.add(attr);
      }
      return matches;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting attributes using this where clause: " + whereClause, ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Get all artifacts that match a where clause. The clause should begin with
   * "WHERE" or "JOIN". To use this method you must know the database tables
   *
   * @param whereClause a sqlite where clause
   * @return a list of matching artifacts
   * @throws TskCoreException exception thrown if a critical error occurs
   * within tsk core
   */
  public ArrayList<BlackboardArtifact> getMatchingArtifacts(String whereClause) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet rs = null;
    Statement s = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT artifact_id, obj_id, artifact_type_id FROM blackboard_artifacts " + whereClause); //NON-NLS
      ArrayList<BlackboardArtifact> matches = new ArrayList<BlackboardArtifact>();
      while (rs.next()) {
        BlackboardArtifact artifact = new BlackboardArtifact(this, rs.getLong(1), rs.getLong(2), rs.getInt(3), this.getArtifactTypeString(rs.getInt(3)), this.getArtifactTypeDisplayName(rs.getInt(3)));
        matches.add(artifact);
      }
      return matches;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting attributes using this where clause: " + whereClause, ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Add a new blackboard artifact with the given type. If that artifact type
   * does not exist an error will be thrown. The artifact type name can be
   * looked up in the returned blackboard artifact.
   *
   * @param artifactTypeID the type the given artifact should have
   * @param obj_id the content object id associated with this artifact
   * @return a new blackboard artifact
   * @throws TskCoreException exception thrown if a critical error occurs
   * within tsk core
   */
  public BlackboardArtifact newBlackboardArtifact(int artifactTypeID, long obj_id) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireExclusiveLock();
    ResultSet rs = null;
    try {
      String artifactTypeName = getArtifactTypeString(artifactTypeID);
      String artifactDisplayName = getArtifactTypeDisplayName(artifactTypeID);
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.INSERT_ARTIFACT);
      statement.clearParameters();
      statement.setLong(1, obj_id);
      statement.setInt(2, artifactTypeID);
      connection.executeUpdate(statement);
      rs = statement.getGeneratedKeys();
      return new BlackboardArtifact(this, rs.getLong(1), obj_id, artifactTypeID, artifactTypeName, artifactDisplayName);
    } catch (SQLException ex) {
      throw new TskCoreException("Error creating a blackboard artifact", ex);
    } finally {
      closeResultSet(rs);
      releaseExclusiveLock();
    }
  }

  /**
   * Add a new blackboard artifact with the given type.
   *
   * @param artifactType the type the given artifact should have
   * @param obj_id the content object id associated with this artifact
   * @return a new blackboard artifact
   * @throws TskCoreException exception thrown if a critical error occurs
   * within tsk core
   */
  public BlackboardArtifact newBlackboardArtifact(ARTIFACT_TYPE artifactType, long obj_id) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireExclusiveLock();
    ResultSet rs = null;
    try {
      final int type = artifactType.getTypeID();
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.INSERT_ARTIFACT);
      statement.clearParameters();
      statement.setLong(1, obj_id);
      statement.setInt(2, type);
      connection.executeUpdate(statement);
      rs = statement.getGeneratedKeys();
      return new BlackboardArtifact(this, rs.getLong(1), obj_id, type, artifactType.getLabel(), artifactType.getDisplayName());
    } catch (SQLException ex) {
      throw new TskCoreException("Error creating a blackboard artifact", ex);
    } finally {
      closeResultSet(rs);
      releaseExclusiveLock();
    }
  }

  /**
   * Checks if the content object has children. Note: this is generally more
   * efficient then preloading all children and checking if the set is empty,
   * and facilities lazy loading.
   *
   * @param content content object to check for children
   * @return true if has children, false otherwise
   * @throws TskCoreException exception thrown if a critical error occurs
   * within tsk core
   */
  boolean getContentHasChildren(Content content) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet rs = null;
    try {
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.COUNT_CHILD_OBJECTS_BY_PARENT);
      statement.clearParameters();
      statement.setLong(1, content.getId());
      rs = connection.executeQuery(statement);
      boolean hasChildren = false;
      if (rs.next()) {
        hasChildren = rs.getInt(1) > 0;
      }
      return hasChildren;
    } catch (SQLException e) {
      throw new TskCoreException("Error checking for children of parent " + content, e);
    } finally {
      closeResultSet(rs);
      releaseSharedLock();
    }
  }

  /**
   * Counts if the content object children. Note: this is generally more
   * efficient then preloading all children and counting, and facilities lazy
   * loading.
   *
   * @param content content object to check for children count
   * @return children count
   * @throws TskCoreException exception thrown if a critical error occurs
   * within tsk core
   */
  int getContentChildrenCount(Content content) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet rs = null;
    try {
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.COUNT_CHILD_OBJECTS_BY_PARENT);
      statement.clearParameters();
      statement.setLong(1, content.getId());
      rs = connection.executeQuery(statement);
      int countChildren = -1;
      if (rs.next()) {
        countChildren = rs.getInt(1);
      }
      return countChildren;
    } catch (SQLException e) {
      throw new TskCoreException("Error checking for children of parent " + content, e);
    } finally {
      closeResultSet(rs);
      releaseSharedLock();
    }
  }

  /**
   * Returns the list of AbstractFile Children of a given type for a given
   * AbstractFileParent
   *
   * @param parent the content parent to get abstract file children for
   * @param type children type to look for, defined in TSK_DB_FILES_TYPE_ENUM
   * @throws TskCoreException exception thrown if a critical error occurs
   * within tsk core
   */
  List<Content> getAbstractFileChildren(Content parent, TSK_DB_FILES_TYPE_ENUM type) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet rs = null;
    try {
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_FILES_BY_PARENT_AND_TYPE);
      statement.clearParameters();
      long parentId = parent.getId();
      statement.setLong(1, parentId);
      statement.setShort(2, type.getFileType());
      rs = connection.executeQuery(statement);
      return rsHelper.fileChildren(rs, parentId);
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting AbstractFile children for Content", ex);
    } finally {
      closeResultSet(rs);
      releaseSharedLock();
    }
  }

  /**
   * Returns the list of all AbstractFile Children for a given
   * AbstractFileParent
   *
   * @param parent the content parent to get abstract file children for
   * @param type children type to look for, defined in TSK_DB_FILES_TYPE_ENUM
   * @throws TskCoreException exception thrown if a critical error occurs
   * within tsk core
   */
  List<Content> getAbstractFileChildren(Content parent) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet rs = null;
    try {
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_FILES_BY_PARENT);
      statement.clearParameters();
      long parentId = parent.getId();
      statement.setLong(1, parentId);
      rs = connection.executeQuery(statement);
      return rsHelper.fileChildren(rs, parentId);
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting AbstractFile children for Content", ex);
    } finally {
      closeResultSet(rs);
      releaseSharedLock();
    }
  }

  /**
   * Get list of IDs for abstract files of a given type that are children of a
   * given content.
   *
   * @param parent Object to find children for
   * @param type Type of children to find IDs for
   * @return
   * @throws TskCoreException
   */
  List<Long> getAbstractFileChildrenIds(Content parent, TSK_DB_FILES_TYPE_ENUM type) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet rs = null;
    try {
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_FILE_IDS_BY_PARENT_AND_TYPE);
      statement.clearParameters();
      statement.setLong(1, parent.getId());
      statement.setShort(2, type.getFileType());
      rs = connection.executeQuery(statement);
      List<Long> children = new ArrayList<Long>();
      while (rs.next()) {
        children.add(rs.getLong(1));
      }
      return children;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting AbstractFile children for Content", ex);
    } finally {
      closeResultSet(rs);
      releaseSharedLock();
    }
  }

  /**
   * Get list of IDs for abstract files that are children of a given content.
   *
   * @param parent Object to find children for
   * @return
   * @throws TskCoreException
   */
  List<Long> getAbstractFileChildrenIds(Content parent) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet rs = null;
    try {
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_FILE_IDS_BY_PARENT);
      statement.clearParameters();
      statement.setLong(1, parent.getId());
      rs = connection.executeQuery(statement);
      List<Long> children = new ArrayList<Long>();
      while (rs.next()) {
        children.add(rs.getLong(1));
      }
      return children;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting AbstractFile children for Content", ex);
    } finally {
      closeResultSet(rs);
      releaseSharedLock();
    }
  }

  /**
   * Stores a pair of object ID and its type
   */
  static class ObjectInfo {

    long id;
    TskData.ObjectType type;

    ObjectInfo(long id, ObjectType type) {
      this.id = id;
      this.type = type;
    }
  }

  /**
   * Get info about children of a given Content from the database. TODO: the
   * results of this method are volumes, file systems, and fs files.
   *
   * @param c Parent object to run query against
   * @throws TskCoreException exception thrown if a critical error occurs
   * within tsk core
   */
  Collection<ObjectInfo> getChildrenInfo(Content c) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT tsk_objects.obj_id, tsk_objects.type " //NON-NLS
          + "FROM tsk_objects left join tsk_files " //NON-NLS
          + "ON tsk_objects.obj_id=tsk_files.obj_id " //NON-NLS
          + "WHERE tsk_objects.par_obj_id = " + c.getId()); //NON-NLS
      Collection<ObjectInfo> infos = new ArrayList<ObjectInfo>();
      while (rs.next()) {
        infos.add(new ObjectInfo(rs.getLong("obj_id"), ObjectType.valueOf(rs.getShort("type")))); //NON-NLS
      }
      return infos;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting Children Info for Content", ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Get parent info for the parent of the content object
   *
   * @param c content object to get parent info for
   * @return the parent object info with the parent object type and id
   * @throws TskCoreException exception thrown if a critical error occurs
   * within tsk core
   */
  ObjectInfo getParentInfo(Content c) throws TskCoreException {
    // TODO: This should not throw an exception if Content has no parent,
    // return null instead.
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT parent.obj_id, parent.type " //NON-NLS
          + "FROM tsk_objects AS parent INNER JOIN tsk_objects AS child " //NON-NLS
          + "ON child.par_obj_id = parent.obj_id " //NON-NLS
          + "WHERE child.obj_id = " + c.getId()); //NON-NLS
      if (rs.next()) {
        return new ObjectInfo(rs.getLong(1), ObjectType.valueOf(rs.getShort(2)));
      } else {
        throw new TskCoreException("Given content (id: " + c.getId() + ") has no parent");
      }
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting Parent Info for Content", ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Get parent info for the parent of the content object id
   *
   * @param id content object id to get parent info for
   * @return the parent object info with the parent object type and id
   * @throws TskCoreException exception thrown if a critical error occurs
   * within tsk core
   */
  ObjectInfo getParentInfo(long contentId) throws TskCoreException {
    // TODO: This should not throw an exception if Content has no parent,
    // return null instead.
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT parent.obj_id, parent.type " //NON-NLS
          + "FROM tsk_objects AS parent INNER JOIN tsk_objects AS child " //NON-NLS
          + "ON child.par_obj_id = parent.obj_id " //NON-NLS
          + "WHERE child.obj_id = " + contentId); //NON-NLS
      if (rs.next()) {
        return new ObjectInfo(rs.getLong(1), ObjectType.valueOf(rs.getShort(2)));
      } else {
        throw new TskCoreException("Given content (id: " + contentId + ") has no parent.");
      }
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting Parent Info for Content: " + contentId, ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Gets parent directory for FsContent object
   *
   * @param fsc FsContent to get parent dir for
   * @return the parent Directory
   * @throws TskCoreException thrown if critical error occurred within tsk
   * core
   */
  Directory getParentDirectory(FsContent fsc) throws TskCoreException {
    // TODO: This should not throw an exception if Content has no parent,
    // return null instead.
    if (fsc.isRoot()) {
      throw new TskCoreException("Given FsContent (id: " + fsc.getId() + ") is a root object (can't have parent directory).");
    } else {
      ObjectInfo parentInfo = getParentInfo(fsc);
      Directory parent = null;
      if (parentInfo.type == ObjectType.ABSTRACTFILE) {
        parent = getDirectoryById(parentInfo.id, fsc.getFileSystem());
      } else {
        throw new TskCoreException("Parent of FsContent (id: " + fsc.getId() + ") has wrong type to be directory: " + parentInfo.type);
      }
      return parent;
    }
  }

  /**
   * Get content object by content id
   *
   * @param id to get content object for
   * @return instance of a Content object (one of its subclasses), or null if
   * not found.
   * @throws TskCoreException thrown if critical error occurred within tsk
   * core
   */
  public Content getContentById(long id) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT * FROM tsk_objects WHERE obj_id = " + id + " LIMIT  1"); //NON-NLS
      if (!rs.next()) {
        return null;
      }

      AbstractContent content = null;
      long parentId = rs.getLong("par_obj_id"); //NON-NLS
      final TskData.ObjectType type = TskData.ObjectType.valueOf(rs.getShort("type")); //NON-NLS
      switch (type) {
        case IMG:
          content = getImageById(id);
          break;
        case VS:
          content = getVolumeSystemById(id, parentId);
          break;
        case VOL:
          content = getVolumeById(id, parentId);
          break;
        case FS:
          content = getFileSystemById(id, parentId);
          break;
        case ABSTRACTFILE:
          content = getAbstractFileById(id);
          break;
        default:
          throw new TskCoreException("Could not obtain Content object with ID: " + id);
      }
      return content;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting Content by ID.", ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Get a path of a file in tsk_files_path table or null if there is none
   *
   * @param id id of the file to get path for
   * @return file path or null
   */
  String getFilePath(long id) {
    CaseDbConnection connection;
    try {
      connection = connections.getConnection();
    } catch (TskCoreException ex) {
      logger.log(Level.SEVERE, "Error getting file path for file " + id, ex); //NON-NLS     
      return null;
    }
    String filePath = null;
    acquireSharedLock();
    ResultSet rs = null;
    try {
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_LOCAL_PATH_FOR_FILE);
      statement.clearParameters();
      statement.setLong(1, id);
      rs = connection.executeQuery(statement);
      if (rs.next()) {
        filePath = rs.getString(1);
      }
    } catch (SQLException ex) {
      logger.log(Level.SEVERE, "Error getting file path for file " + id, ex); //NON-NLS
    } finally {
      closeResultSet(rs);
      releaseSharedLock();
    }
    return filePath;
  }

  /**
   * Get a parent_path of a file in tsk_files table or null if there is none
   *
   * @param id id of the file to get path for
   * @return file path or null
   */
  String getFileParentPath(long id) {
    CaseDbConnection connection;
    try {
      connection = connections.getConnection();
    } catch (TskCoreException ex) {
      logger.log(Level.SEVERE, "Error getting parent file path for file " + id, ex); //NON-NLS     
      return null;
    }
    String parentPath = null;
    acquireSharedLock();
    ResultSet rs = null;
    try {
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_PATH_FOR_FILE);
      statement.clearParameters();
      statement.setLong(1, id);
      rs = connection.executeQuery(statement);
      if (rs.next()) {
        parentPath = rs.getString(1);
      }
    } catch (SQLException ex) {
      logger.log(Level.SEVERE, "Error getting file parent_path for file " + id, ex); //NON-NLS
    } finally {
      closeResultSet(rs);
      releaseSharedLock();
    }
    return parentPath;
  }

  /**
   * Get a name of a file in tsk_files table or null if there is none
   *
   * @param id id of the file to get name for
   * @return file name or null
   */
  String getFileName(long id) {
    CaseDbConnection connection;
    try {
      connection = connections.getConnection();
    } catch (TskCoreException ex) {
      logger.log(Level.SEVERE, "Error getting file name for file " + id, ex); //NON-NLS     
      return null;
    }
    String fileName = null;
    acquireSharedLock();
    ResultSet rs = null;
    try {
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_FILE_NAME);
      statement.clearParameters();
      statement.setLong(1, id);
      rs = connection.executeQuery(statement);
      if (rs.next()) {
        fileName = rs.getString(1);
      }
    } catch (SQLException ex) {
      logger.log(Level.SEVERE, "Error getting file parent_path for file " + id, ex); //NON-NLS
    } finally {
      closeResultSet(rs);
      releaseSharedLock();
    }
    return fileName;
  }

  /**
   * Get a derived method for a file, or null if none
   *
   * @param id id of the derived file
   * @return derived method or null if not present
   * @throws TskCoreException exception throws if core error occurred and
   * method could not be queried
   */
  DerivedFile.DerivedMethod getDerivedMethod(long id) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    DerivedFile.DerivedMethod method = null;
    acquireSharedLock();
    ResultSet rs1 = null;
    ResultSet rs2 = null;
    try {
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_DERIVED_FILE);
      statement.clearParameters();
      statement.setLong(1, id);
      rs1 = connection.executeQuery(statement);
      if (rs1.next()) {
        int method_id = rs1.getInt(1);
        String rederive = rs1.getString(1);
        method = new DerivedFile.DerivedMethod(method_id, rederive);
        statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_FILE_DERIVATION_METHOD);
        statement.clearParameters();
        statement.setInt(1, method_id);
        rs2 = connection.executeQuery(statement);
        if (rs2.next()) {
          method.setToolName(rs2.getString(1));
          method.setToolVersion(rs2.getString(2));
          method.setOther(rs2.getString(3));
        }
      }
    } catch (SQLException e) {
      logger.log(Level.SEVERE, "Error getting derived method for file: " + id, e); //NON-NLS
    } finally {
      closeResultSet(rs2);
      closeResultSet(rs1);
      releaseSharedLock();
    }
    return method;
  }

  /**
   * Get abstract file object from tsk_files table by its id
   *
   * @param id id of the file object in tsk_files table
   * @return AbstractFile object populated, or null if not found.
   * @throws TskCoreException thrown if critical error occurred within tsk
   * core and file could not be queried
   */
  public AbstractFile getAbstractFileById(long id) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet rs = null;
    try {
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_FILE_BY_ID);
      statement.clearParameters();
      statement.setLong(1, id);
      rs = connection.executeQuery(statement);
      List<AbstractFile> results;
      if ((results = resultSetToAbstractFiles(rs)).size() > 0) {
        return results.get(0);
      } else {
        return null;
      }
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting file by id, id = " + id, ex);
    } finally {
      closeResultSet(rs);
      releaseSharedLock();
    }
  }

  /**
   * Get the object ID of the file system that a file is located in.
   *
   * Note: for FsContent files, this is the real fs for other non-fs
   * AbstractFile files, this field is used internally for data source id (the
   * root content obj)
   *
   * @param fileId object id of the file to get fs column id for
   * @return fs_id or -1 if not present
   */
  private long getFileSystemId(long fileId) {
    CaseDbConnection connection;
    try {
      connection = connections.getConnection();
    } catch (TskCoreException ex) {
      logger.log(Level.SEVERE, "Error getting file system id for file " + fileId, ex); //NON-NLS     
      return -1;
    }
    acquireSharedLock();
    ResultSet rs = null;
    long ret = -1;
    try {
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_FILE_SYSTEM_BY_OBJECT);
      statement.clearParameters();
      statement.setLong(1, fileId);
      rs = connection.executeQuery(statement);
      if (rs.next()) {
        ret = rs.getLong(1);
        if (ret == 0) {
          ret = -1;
        }
      }
    } catch (SQLException e) {
      logger.log(Level.SEVERE, "Error checking file system id of a file, id = " + fileId, e); //NON-NLS
    } finally {
      closeResultSet(rs);
      releaseSharedLock();
    }
    return ret;
  }



  /**
   * Checks if the file is a (sub)child of the data source (parentless Content
   * object such as Image or VirtualDirectory representing filesets)
   *
   * @param dataSource dataSource to check
   * @param fileId id of file to check
   * @return true if the file is in the dataSource hierarchy
   * @throws TskCoreException thrown if check failed
   */
  public boolean isFileFromSource(Content dataSource, long fileId) throws TskCoreException {
    if (dataSource.getParent() != null) {
      final String msg = MessageFormat.format(bundle.getString("SleuthkitCase.isFileFromSource.exception.msg.text"), dataSource);
      logger.log(Level.SEVERE, msg);
      throw new IllegalArgumentException(msg);
    }

    //get fs_id for file id
    long fsId = getFileSystemId(fileId);
    if (fsId == -1) {
      return false;
    }

    //if image, check if one of fs in data source
    if (dataSource instanceof Image) {
      Collection<FileSystem> fss = getFileSystems((Image) dataSource);
      for (FileSystem fs : fss) {
        if (fs.getId() == fsId) {
          return true;
        }
      }
      return false;
    } //if VirtualDirectory, check if dataSource id is the fs_id
    else if (dataSource instanceof VirtualDirectory) {
      //fs_obj_id is not a real fs in this case
      //we are currently using this field internally to get to data source of non-fs files quicker
      //this will be fixed in 2.5 schema
      return dataSource.getId() == fsId;
    } else {
      final String msg = MessageFormat.format(bundle.getString("SleuthkitCase.isFileFromSource.exception.msg2.text"), dataSource);
      logger.log(Level.SEVERE, msg);
      throw new IllegalArgumentException(msg);
    }
  }

  /**
   * @param dataSource the dataSource (Image, parent-less VirtualDirectory) to
   * search for the given file name
   * @param fileName Pattern of the name of the file or directory to match
   * (case insensitive, used in LIKE SQL statement).
   * @return a list of AbstractFile for files/directories whose name matches
   * the given fileName
   * @throws TskCoreException thrown if check failed
   */
  public List<AbstractFile> findFiles(Content dataSource, String fileName) throws TskCoreException {
    if (dataSource.getParent() != null) {
      final String msg = MessageFormat.format(bundle.getString("SleuthkitCase.isFileFromSource.exception.msg1.text"), dataSource);
      logger.log(Level.SEVERE, msg);
      throw new IllegalArgumentException(msg);
    }

    List<AbstractFile> files = new ArrayList<AbstractFile>();
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet rs = null;
    try {
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_FILES_BY_FILE_SYSTEM_AND_NAME);
      statement.clearParameters();
      if (dataSource instanceof Image) {
        for (FileSystem fileSystem : getFileSystems((Image) dataSource)) {
          statement.setString(1, fileName.toLowerCase());
          statement.setLong(2, fileSystem.getId());
          rs = connection.executeQuery(statement);
          files.addAll(resultSetToAbstractFiles(rs));
        }
      } else if (dataSource instanceof VirtualDirectory) {
        //fs_obj_id is special for non-fs files (denotes data source)
        statement.setString(1, fileName.toLowerCase());
        statement.setLong(2, dataSource.getId());
        rs = connection.executeQuery(statement);
        files = resultSetToAbstractFiles(rs);
      } else {
        final String msg = MessageFormat.format(bundle.getString("SleuthkitCase.findFiles.exception.msg2.text"), dataSource);
        logger.log(Level.SEVERE, msg);
        throw new IllegalArgumentException(msg);
      }
    } catch (SQLException e) {
      throw new TskCoreException(bundle.getString("SleuthkitCase.findFiles.exception.msg3.text"), e);
    } finally {
      closeResultSet(rs);
      releaseSharedLock();
    }
    return files;
  }

  /**
   * @param dataSource the dataSource (Image, parent-less VirtualDirectory) to
   * search for the given file name
   * @param fileName Pattern of the name of the file or directory to match
   * (case insensitive, used in LIKE SQL statement).
   * @param dirName Pattern of the name of a parent directory of fileName
   * (case insensitive, used in LIKE SQL statement)
   * @return a list of AbstractFile for files/directories whose name matches
   * fileName and whose parent directory contains dirName.
   * @throws org.sleuthkit.datamodel.TskCoreException
   */
  public List<AbstractFile> findFiles(Content dataSource, String fileName, String dirName) throws TskCoreException {
    if (dataSource.getParent() != null) {
      final String msg = MessageFormat.format(bundle.getString("SleuthkitCase.findFiles3.exception.msg1.text"), dataSource);
      logger.log(Level.SEVERE, msg);
      throw new IllegalArgumentException(msg);
    }

    List<AbstractFile> files = new ArrayList<AbstractFile>();
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet rs = null;
    try {
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_FILES_BY_FILE_SYSTEM_AND_PATH);
      statement.clearParameters();
      if (dataSource instanceof Image) {
        for (FileSystem fileSystem : getFileSystems((Image) dataSource)) {
          statement.setString(1, fileName.toLowerCase());
          statement.setString(2, "%" + dirName.toLowerCase() + "%"); //NON-NLS
          statement.setLong(3, fileSystem.getId());
          rs = connection.executeQuery(statement);
          files.addAll(resultSetToAbstractFiles(rs));
        }
      } else if (dataSource instanceof VirtualDirectory) {
        statement.setString(1, fileName.toLowerCase());
        statement.setString(2, "%" + dirName.toLowerCase() + "%"); //NON-NLS
        statement.setLong(3, dataSource.getId());
        rs = connection.executeQuery(statement);
        files = resultSetToAbstractFiles(rs);
      } else {
        final String msg = MessageFormat.format(bundle.getString("SleuthkitCase.findFiles3.exception.msg2.text"), dataSource);
        logger.log(Level.SEVERE, msg);
        throw new IllegalArgumentException(msg);
      }
    } catch (SQLException e) {
      throw new TskCoreException(bundle.getString("SleuthkitCase.findFiles3.exception.msg3.text"), e);
    } finally {
      if (rs != null) {
        try {
          rs.close();
        } catch (SQLException ex) {
          logger.log(Level.WARNING, "Error closing result set after finding files", ex); //NON-NLS
        }
      }
      releaseSharedLock();
    }
    return files;
  }

  /**
   * wraps the version of addVirtualDirectory that takes a Transaction in a
   * transaction local to this method
   *
   * @param parentId
   * @param directoryName
   * @return
   * @throws TskCoreException
   */
  public VirtualDirectory addVirtualDirectory(long parentId, String directoryName) throws TskCoreException {
    acquireExclusiveLock();
    CaseDbTransaction localTrans = beginTransaction();
    try {
      VirtualDirectory newVD = addVirtualDirectory(parentId, directoryName, localTrans);
      localTrans.commit();
      return newVD;
    } catch (TskCoreException ex) {
      localTrans.rollback();
      throw ex;
    } finally {
      releaseExclusiveLock();
    }
  }

  /**
   * Adds a virtual directory to the database and returns a VirtualDirectory
   * object representing it.
   *
   * @param parentId the ID of the parent, or 0 if NULL
   * @param directoryName the name of the virtual directory to create
   * @param trans the transaction in the scope of which the operation is to be
   * performed, managed by the caller
   * @return a VirtualDirectory object representing the one added to the
   * database.
   * @throws TskCoreException
   */
  public VirtualDirectory addVirtualDirectory(long parentId, String directoryName, CaseDbTransaction trans) throws TskCoreException {
    if (trans == null) {
      throw new TskCoreException("Passed null CaseDbTransaction");
    }

    acquireExclusiveLock();
    ResultSet resultSet = null;
    try {
      // Get the parent path.
      String parentPath = getFileParentPath(parentId);
      if (parentPath == null) {
        parentPath = ""; //NON-NLS
      }
      String parentName = getFileName(parentId);
      if (parentName != null) {
        parentPath = parentPath + "/" + parentName; //NON-NLS
      }

      // Insert a row for the virtual directory into the tsk_objects table.
      // INSERT INTO tsk_objects (par_obj_id, type) VALUES (?, ?)
      CaseDbConnection connection = trans.getConnection();
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.INSERT_OBJECT);
      statement.clearParameters();
      if (parentId != 0) {
        statement.setLong(1, parentId);
      }
      statement.setLong(2, TskData.ObjectType.ABSTRACTFILE.getObjectType());
      connection.executeUpdate(statement);
      resultSet = statement.getGeneratedKeys();
      long newObjId = resultSet.getLong(1);

      // Insert a row for the virtual directory into the tsk_files table.
      // INSERT INTO tsk_files (obj_id, fs_obj_id, name, type, has_path, dir_type, meta_type,
      // dir_flags, meta_flags, size, ctime, crtime, atime, mtime, parent_path)
      // VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)     
      statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.INSERT_FILE);
      statement.clearParameters();
      statement.setLong(1, newObjId);

      // If the parent is part of a file system, grab its file system ID
      long parentFs = this.getFileSystemId(parentId);
      if (parentFs != -1) {
        statement.setLong(2, parentFs);
      }
      statement.setString(3, directoryName);

      //type, has_path
      statement.setShort(4, TskData.TSK_DB_FILES_TYPE_ENUM.VIRTUAL_DIR.getFileType());
      statement.setBoolean(5, true);

      //flags
      final TSK_FS_NAME_TYPE_ENUM dirType = TSK_FS_NAME_TYPE_ENUM.DIR;
      statement.setShort(6, dirType.getValue());
      final TSK_FS_META_TYPE_ENUM metaType = TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_DIR;
      statement.setShort(7, metaType.getValue());

      //note: using alloc under assumption that derived files derive from alloc files
      final TSK_FS_NAME_FLAG_ENUM dirFlag = TSK_FS_NAME_FLAG_ENUM.ALLOC;
      statement.setShort(8, dirFlag.getValue());
      final short metaFlags = (short) (TSK_FS_META_FLAG_ENUM.ALLOC.getValue()
          | TSK_FS_META_FLAG_ENUM.USED.getValue());
      statement.setShort(9, metaFlags);

      //size
      long size = 0;
      statement.setLong(10, size);

      //parent path, nulls for params 11-14
      statement.setString(15, parentPath);

      connection.executeUpdate(statement);

      return new VirtualDirectory(this, newObjId, directoryName, dirType,
          metaType, dirFlag, metaFlags, size, null, FileKnown.UNKNOWN,
          parentPath);
    } catch (SQLException e) {
      throw new TskCoreException("Error creating virtual directory '" + directoryName + "'", e);
    } finally {
      closeResultSet(resultSet);
      releaseExclusiveLock();
    }
  }

  /**
   * Get IDs of the virtual folder roots (at the same level as image), used
   * for containers such as for local files.
   *
   * @return IDs of virtual directory root objects.
   * @throws org.sleuthkit.datamodel.TskCoreException
   */
  public List<VirtualDirectory> getVirtualDirectoryRoots() throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT tsk_files.* FROM tsk_objects, tsk_files WHERE " //NON-NLS
          + "tsk_objects.par_obj_id IS NULL AND " //NON-NLS
          + "tsk_objects.type = " + TskData.ObjectType.ABSTRACTFILE.getObjectType() + " AND " //NON-NLS
          + "tsk_objects.obj_id = tsk_files.obj_id AND " //NON-NLS
          + "tsk_files.type = " + TskData.TSK_DB_FILES_TYPE_ENUM.VIRTUAL_DIR.getFileType()
          + " ORDER BY tsk_files.dir_type, tsk_files.name COLLATE NOCASE"); //NON-NLS
      List<VirtualDirectory> virtDirRootIds = new ArrayList<VirtualDirectory>();
      while (rs.next()) {
        virtDirRootIds.add(rsHelper.virtualDirectory(rs));
      }
      return virtDirRootIds;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting local files virtual folder id", ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Adds a carved file to the VirtualDirectory '$CarvedFiles' in the volume
   * or image given by systemId. Creates $CarvedFiles virtual directory if it
   * does not exist already.
   *
   * @param carvedFileName the name of the carved file to add
   * @param carvedFileSize the size of the carved file to add
   * @param containerId the ID of the parent volume, file system, or image
   * @param data the layout information - a list of offsets that make up this
   * carved file.
   * @return A LayoutFile object representing the carved file.
   * @throws org.sleuthkit.datamodel.TskCoreException
   */
  public LayoutFile addCarvedFile(String carvedFileName, long carvedFileSize, long containerId, List<TskFileRange> data) throws TskCoreException {

    List<CarvedFileContainer> carvedFileContainer = new ArrayList<CarvedFileContainer>();
    carvedFileContainer.add(new CarvedFileContainer(carvedFileName, carvedFileSize, containerId, data));

    List<LayoutFile> layoutCarvedFiles = addCarvedFiles(carvedFileContainer);
    if (layoutCarvedFiles != null) {
      return layoutCarvedFiles.get(0);
    } else {
      return null;
    }
  }

  /**
   * Adds a collection of carved files to the VirtualDirectory '$CarvedFiles'
   * in the volume or image given by systemId. Creates $CarvedFiles virtual
   * directory if it does not exist already.
   *
   * @param filesToAdd a list of CarvedFileContainer files to add as carved
   * files
   * @return List<LayoutFile> This is a list of the files added to the
   * database
   * @throws org.sleuthkit.datamodel.TskCoreException
   */
  public List<LayoutFile> addCarvedFiles(List<CarvedFileContainer> filesToAdd) throws TskCoreException {
    if (filesToAdd != null && filesToAdd.isEmpty() == false) {
      CaseDbTransaction localTrans = beginTransaction();
      CaseDbConnection connection = localTrans.getConnection();
      acquireExclusiveLock();
      Statement s = null;
      ResultSet rs = null;
      List<LayoutFile> addedFiles = new ArrayList<LayoutFile>();

      try {
        // get the ID of the appropriate '$CarvedFiles' directory
        long firstItemId = filesToAdd.get(0).getId();
        long id = 0;
        // first, check the cache
        Long carvedDirId = carvedFileContainersCache.get(firstItemId);
        if (carvedDirId != null) {
          id = carvedDirId;
        } else {
          // it's not in the cache. Go to the DB
          // determine if we've got a volume system or file system ID
          Content parent = getContentById(firstItemId);
          if (parent == null) {
            throw new TskCoreException("No Content object found with this ID (" + firstItemId + ").");
          }

          List<Content> children = Collections.<Content>emptyList();
          if (parent instanceof FileSystem) {
            FileSystem fs = (FileSystem) parent;
            children = fs.getRootDirectory().getChildren();
          } else if (parent instanceof Volume
              || parent instanceof Image) {
            children = parent.getChildren();
          } else {
            throw new TskCoreException("The given ID (" + firstItemId + ") was not an image, volume or file system.");
          }

          // see if any of the children are a '$CarvedFiles' directory
          Content carvedFilesDir = null;
          for (Content child : children) {
            if (child.getName().equals(VirtualDirectory.NAME_CARVED)) {
              carvedFilesDir = child;
              break;
            }
          }

          // if we found it, add it to the cache and grab its ID
          if (carvedFilesDir != null) {
            // add it to the cache
            carvedFileContainersCache.put(firstItemId, carvedFilesDir.getId());
            id = carvedFilesDir.getId();
          } else {
            // a carved files directory does not exist; create one
            VirtualDirectory vd = addVirtualDirectory(firstItemId, VirtualDirectory.NAME_CARVED, localTrans);
            id = vd.getId();
            // add it to the cache
            carvedFileContainersCache.put(firstItemId, id);
          }
        }

        // get the parent path for the $CarvedFiles directory   
        String parentPath = getFileParentPath(id);
        if (parentPath == null) {
          parentPath = ""; //NON-NLS
        }
        String parentName = getFileName(id);
        if (parentName != null) {
          parentPath = parentPath + "/" + parentName; //NON-NLS
        }

        // we should cache this when we start adding lots of carved files...
        boolean isContainerAFs = false;
        s = connection.createStatement();
        rs = connection.executeQuery(s, "select * from tsk_fs_info " //NON-NLS
            + "where obj_id = " + firstItemId); //NON-NLS
        if (rs.next()) {
          isContainerAFs = true;
        }
        rs.close();
        rs = null;

        for (CarvedFileContainer itemToAdd : filesToAdd) {

          // Insert a row for the carved file into the tsk_objects table.
          // INSERT INTO tsk_objects (par_obj_id, type) VALUES (?, ?)
          PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.INSERT_OBJECT);
          statement.clearParameters();
          statement.setLong(1, id);
          statement.setLong(2, TskData.ObjectType.ABSTRACTFILE.getObjectType());
          connection.executeUpdate(statement);
          rs = statement.getGeneratedKeys();
          long newObjId = rs.getLong(1);

          // Insert a row for the carved file into the tsk_files table.
          // INSERT INTO tsk_files (obj_id, fs_obj_id, name, type, has_path, dir_type, meta_type,
          // dir_flags, meta_flags, size, ctime, crtime, atime, mtime, parent_path)
          // VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)     
          statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.INSERT_FILE);
          statement.clearParameters();
          statement.setLong(1, newObjId);

          // only insert into the fs_obj_id column if container is a FS
          if (isContainerAFs) {
            statement.setLong(2, itemToAdd.getId());
          }
          statement.setString(3, itemToAdd.getName());

          // type
          final TSK_DB_FILES_TYPE_ENUM type = TSK_DB_FILES_TYPE_ENUM.CARVED;
          statement.setShort(4, type.getFileType());

          // has_path
          statement.setBoolean(5, true);

          // dirType
          final TSK_FS_NAME_TYPE_ENUM dirType = TSK_FS_NAME_TYPE_ENUM.REG;
          statement.setShort(6, dirType.getValue());

          // metaType
          final TSK_FS_META_TYPE_ENUM metaType = TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_REG;
          statement.setShort(7, metaType.getValue());

          // dirFlag
          final TSK_FS_NAME_FLAG_ENUM dirFlag = TSK_FS_NAME_FLAG_ENUM.UNALLOC;
          statement.setShort(8, dirFlag.getValue());

          // metaFlags
          final short metaFlags = TSK_FS_META_FLAG_ENUM.UNALLOC.getValue();
          statement.setShort(9, metaFlags);

          // size
          statement.setLong(10, itemToAdd.getSize());

          //parent path, nulls for params 11-14
          statement.setString(15, parentPath);

          connection.executeUpdate(statement);

          // Add a row in the tsk_layout_file table for each TskFileRange.
          // INSERT INTO tsk_file_layout (obj_id, byte_start, byte_len, sequence)
          // VALUES (?, ?, ?, ?)
          statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.INSERT_LAYOUT_FILE);
          for (TskFileRange tskFileRange : itemToAdd.getRanges()) {
            statement.clearParameters();

            // set the object ID
            statement.setLong(1, newObjId);

            // set byte_start
            statement.setLong(2, tskFileRange.getByteStart());

            // set byte_len
            statement.setLong(3, tskFileRange.getByteLen());

            // set the sequence number
            statement.setLong(4, tskFileRange.getSequence());

            // execute it
            connection.executeUpdate(statement);
          }

          addedFiles.add(new LayoutFile(this, newObjId, itemToAdd.getName(),
              type, dirType, metaType, dirFlag, metaFlags,
              itemToAdd.getSize(), null, FileKnown.UNKNOWN, parentPath));
        }
        localTrans.commit();
        return addedFiles;
      } catch (SQLException ex) {
        localTrans.rollback();
        throw new TskCoreException("Failed to add carved file to case database", ex);
      } finally {
        closeResultSet(rs);
        closeStatement(s);
        releaseExclusiveLock();
      }
    } // if fileToAdd != null
    return null;
  }

  /**
   * Creates a new derived file object, adds it to database and returns it.
   *
   * TODO add support for adding derived method
   *
   * @param fileName file name the derived file
   * @param localPath local path of the derived file, including the file name.
   * The path is relative to the database path.
   * @param size size of the derived file in bytes
   * @param ctime
   * @param crtime
   * @param atime
   * @param mtime
   * @param isFile whether a file or directory, true if a file
   * @param parentFile parent file object (derived or local file)
   * @param rederiveDetails details needed to re-derive file (will be specific
   * to the derivation method), currently unused
   * @param toolName name of derivation method/tool, currently unused
   * @param toolVersion version of derivation method/tool, currently unused
   * @param otherDetails details of derivation method/tool, currently unused
   * @return newly created derived file object
   * @throws TskCoreException exception thrown if the object creation failed
   * due to a critical system error
   */
  public DerivedFile addDerivedFile(String fileName, String localPath,
      long size, long ctime, long crtime, long atime, long mtime,
      boolean isFile, AbstractFile parentFile,
      String rederiveDetails, String toolName, String toolVersion, String otherDetails) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireExclusiveLock();
    ResultSet rs = null;
    try {
      connection.beginTransaction();

      final long parentId = parentFile.getId();
      final String parentPath = parentFile.getParentPath() + parentFile.getName() + '/'; //NON-NLS

      // Insert a row for the derived file into the tsk_objects table.
      // INSERT INTO tsk_objects (par_obj_id, type) VALUES (?, ?)
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.INSERT_OBJECT);
      statement.clearParameters();
      statement.setLong(1, parentId);
      statement.setLong(2, TskData.ObjectType.ABSTRACTFILE.getObjectType());
      connection.executeUpdate(statement);
      rs = statement.getGeneratedKeys();
      long newObjId = rs.getLong(1);
      rs.close();
      rs = null;

      // Insert a row for the virtual directory into the tsk_files table.
      // INSERT INTO tsk_files (obj_id, fs_obj_id, name, type, has_path, dir_type, meta_type,
      // dir_flags, meta_flags, size, ctime, crtime, atime, mtime, parent_path)
      // VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)     
      statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.INSERT_FILE);
      statement.clearParameters();
      statement.setLong(1, newObjId);

      // If the parentFile is part of a file system, use its file system object ID.
      long fsObjId = this.getFileSystemId(parentId);
      if (fsObjId != -1) {
        statement.setLong(2, fsObjId);
      }
      statement.setString(3, fileName);

      //type, has_path
      statement.setShort(4, TskData.TSK_DB_FILES_TYPE_ENUM.DERIVED.getFileType());
      statement.setBoolean(5, true);

      //flags
      final TSK_FS_NAME_TYPE_ENUM dirType = isFile ? TSK_FS_NAME_TYPE_ENUM.REG : TSK_FS_NAME_TYPE_ENUM.DIR;
      statement.setShort(6, dirType.getValue());
      final TSK_FS_META_TYPE_ENUM metaType = isFile ? TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_REG : TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_DIR;
      statement.setShort(7, metaType.getValue());

      //note: using alloc under assumption that derived files derive from alloc files
      final TSK_FS_NAME_FLAG_ENUM dirFlag = TSK_FS_NAME_FLAG_ENUM.ALLOC;
      statement.setShort(8, dirFlag.getValue());
      final short metaFlags = (short) (TSK_FS_META_FLAG_ENUM.ALLOC.getValue()
          | TSK_FS_META_FLAG_ENUM.USED.getValue());
      statement.setShort(9, metaFlags);

      //size
      statement.setLong(10, size);

      //mactimes
      //long ctime, long crtime, long atime, long mtime,
      statement.setLong(11, ctime);
      statement.setLong(12, crtime);
      statement.setLong(13, atime);
      statement.setLong(14, mtime);

      //parent path
      statement.setString(15, parentPath);

      connection.executeUpdate(statement);

      //add localPath
      addFilePath(connection, newObjId, localPath);

      connection.commitTransaction();

      //TODO add derived method to tsk_files_derived and tsk_files_derived_method
      return new DerivedFile(this, newObjId, fileName, dirType, metaType, dirFlag, metaFlags,
          size, ctime, crtime, atime, mtime, null, null, parentPath, localPath, parentId);
    } catch (SQLException ex) {
      connection.rollbackTransaction();
      throw new TskCoreException("Failed to add derived file to case database", ex);
    } finally {
      closeResultSet(rs);
      releaseExclusiveLock();
    }
  }

  /**
   *
   * wraps the version of addLocalFile that takes a Transaction in a
   * transaction local to this method.
   *
   * @param fileName
   * @param localPath
   * @param size
   * @param ctime
   * @param crtime
   * @param atime
   * @param mtime
   * @param isFile
   * @param parent
   * @return
   * @throws TskCoreException
   */
  public LocalFile addLocalFile(String fileName, String localPath,
      long size, long ctime, long crtime, long atime, long mtime,
      boolean isFile, AbstractFile parent) throws TskCoreException {
    acquireExclusiveLock();
    CaseDbTransaction localTrans = beginTransaction();
    try {
      LocalFile created = addLocalFile(fileName, localPath, size, ctime, crtime, atime, mtime, isFile, parent, localTrans);
      localTrans.commit();
      return created;
    } catch (TskCoreException ex) {
      localTrans.rollback();
      throw ex;
    } finally {
      releaseExclusiveLock();
    }
  }

  /**
   * Creates a new local file object, adds it to database and returns it.
   *
   *
   * todo: at the moment we trust the transaction and don't do anything to
   * check it is valid or in the correct state. we should.
   *
   *
   * @param fileName file name the derived file
   * @param localPath local absolute path of the local file, including the
   * file name.
   * @param size size of the derived file in bytes
   * @param ctime
   * @param crtime
   * @param atime
   * @param mtime
   * @param isFile whether a file or directory, true if a file
   * @param parent parent file object (such as virtual directory, another
   * local file, or FsContent type of file)
   * @param trans the transaction in the scope of which the operation is to be
   * performed, managed by the caller
   * @return newly created derived file object
   * @throws TskCoreException exception thrown if the object creation failed
   * due to a critical system error
   */
  public LocalFile addLocalFile(String fileName, String localPath,
      long size, long ctime, long crtime, long atime, long mtime,
      boolean isFile, AbstractFile parent, CaseDbTransaction trans) throws TskCoreException {
    if (trans == null) {
      throw new TskCoreException("Passed null CaseDbTransaction");
    }

    acquireExclusiveLock();
    ResultSet resultSet = null;
    try {
      long parentId = -1;
      String parentPath;
      if (parent == null) {
        throw new TskCoreException(MessageFormat.format(bundle.getString("SleuthkitCase.addLocalFile.exception.msg1.text"), fileName));
      } else {
        parentId = parent.getId();
        parentPath = parent.getParentPath() + "/" + parent.getName(); //NON-NLS
      }

      // Insert a row for the local/logical file into the tsk_objects table.
      // INSERT INTO tsk_objects (par_obj_id, type) VALUES (?, ?)
      CaseDbConnection connection = connections.getConnection();
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.INSERT_OBJECT);
      statement.clearParameters();
      statement.setLong(1, parentId);
      statement.setLong(2, TskData.ObjectType.ABSTRACTFILE.getObjectType());
      connection.executeUpdate(statement);
      resultSet = statement.getGeneratedKeys();
      long newObjId = resultSet.getLong(1);
      resultSet.close();
      resultSet = null;

      // Insert a row for the local/logical file into the tsk_files table.
      // INSERT INTO tsk_files (obj_id, fs_obj_id, name, type, has_path, dir_type, meta_type,
      // dir_flags, meta_flags, size, ctime, crtime, atime, mtime, parent_path)
      // VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)     
      statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.INSERT_FILE);
      statement.clearParameters();
      statement.setLong(1, newObjId);

      // nothing to set for parameter 2, fs_obj_id since local files aren't part of file systems
      statement.setString(3, fileName);

      //type, has_path
      statement.setShort(4, TskData.TSK_DB_FILES_TYPE_ENUM.LOCAL.getFileType());
      statement.setBoolean(5, true);

      //flags
      final TSK_FS_NAME_TYPE_ENUM dirType = isFile ? TSK_FS_NAME_TYPE_ENUM.REG : TSK_FS_NAME_TYPE_ENUM.DIR;
      statement.setShort(6, dirType.getValue());
      final TSK_FS_META_TYPE_ENUM metaType = isFile ? TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_REG : TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_DIR;
      statement.setShort(7, metaType.getValue());

      //note: using alloc under assumption that derived files derive from alloc files
      final TSK_FS_NAME_FLAG_ENUM dirFlag = TSK_FS_NAME_FLAG_ENUM.ALLOC;
      statement.setShort(8, dirFlag.getValue());
      final short metaFlags = (short) (TSK_FS_META_FLAG_ENUM.ALLOC.getValue()
          | TSK_FS_META_FLAG_ENUM.USED.getValue());
      statement.setShort(9, metaFlags);

      //size
      statement.setLong(10, size);

      //mactimes
      //long ctime, long crtime, long atime, long mtime,
      statement.setLong(11, ctime);
      statement.setLong(12, crtime);
      statement.setLong(13, atime);
      statement.setLong(14, mtime);

      //parent path
      statement.setString(15, parentPath);

      connection.executeUpdate(statement);

      //add localPath
      addFilePath(connection, newObjId, localPath);

      return new LocalFile(this, newObjId, fileName, dirType, metaType, dirFlag, metaFlags,
          size, ctime, crtime, atime, mtime, null, null, parentPath, localPath, parentId);
    } catch (SQLException e) {
      throw new TskCoreException("Error adding local file directory " + fileName + " with local path " + localPath, e);
    } finally {
      closeResultSet(resultSet);
      releaseExclusiveLock();
    }
  }

  /**
   * Add a path (such as a local path) for a content object to tsk_file_paths
   *
   * @param objId object id of the file to add the path for
   * @param path the path to add
   * @throws SQLException exception thrown when database error occurred and
   * path was not added
   */
  private void addFilePath(CaseDbConnection connection, long objId, String path) throws SQLException {
    PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.INSERT_LOCAL_PATH);
    statement.clearParameters();
    statement.setLong(1, objId);
    statement.setString(2, path);
    connection.executeUpdate(statement);
  }

  /**
   * Find all files in the data source, by name and parent
   *
   * @param dataSource the dataSource (Image, parent-less VirtualDirectory) to
   * search for the given file name
   * @param fileName Pattern of the name of the file or directory to match
   * (case insensitive, used in LIKE SQL statement).
   * @param parentFile Object for parent file/directory to find children in
   * @return a list of AbstractFile for files/directories whose name matches
   * fileName and that were inside a directory described by parentFile.
   */
  public List<AbstractFile> findFiles(Content dataSource, String fileName, AbstractFile parentFile) throws TskCoreException {
    return findFiles(dataSource, fileName, parentFile.getName());
  }

  /**
   * Count files matching the specific Where clause
   *
   * @param sqlWhereClause a SQL where clause appropriate for the desired
   * files (do not begin the WHERE clause with the word WHERE!)
   * @return count of files each of which satisfy the given WHERE clause
   * @throws TskCoreException
   */
  public long countFilesWhere(String sqlWhereClause) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT COUNT (*) FROM tsk_files WHERE " + sqlWhereClause); //NON-NLS
      return rs.getLong(1);
    } catch (SQLException e) {
      throw new TskCoreException("SQLException thrown when calling 'SleuthkitCase.findFilesWhere().", e);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Find and return list of all (abstract) files matching the specific Where
   * clause
   *
   * @param sqlWhereClause a SQL where clause appropriate for the desired
   * files (do not begin the WHERE clause with the word WHERE!)
   * @return a list of AbstractFile each of which satisfy the given WHERE
   * clause
   * @throws TskCoreException
   */
  public List<AbstractFile> findAllFilesWhere(String sqlWhereClause) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT * FROM tsk_files WHERE " + sqlWhereClause); //NON-NLS
      return resultSetToAbstractFiles(rs);
    } catch (SQLException e) {
      throw new TskCoreException("SQLException thrown when calling 'SleuthkitCase.findAllFilesWhere(): " + sqlWhereClause, e);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Find and return list of all (abstract) ids of files matching the specific
   * Where clause
   *
   * @param sqlWhereClause a SQL where clause appropriate for the desired
   * files (do not begin the WHERE clause with the word WHERE!)
   * @return a list of file ids each of which satisfy the given WHERE clause
   * @throws TskCoreException
   */
  public List<Long> findAllFileIdsWhere(String sqlWhereClause) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT obj_id FROM tsk_files WHERE " + sqlWhereClause); //NON-NLS
      List<Long> ret = new ArrayList<Long>();
      while (rs.next()) {
        ret.add(rs.getLong(1));
      }
      return ret;
    } catch (SQLException e) {
      throw new TskCoreException("SQLException thrown when calling 'SleuthkitCase.findAllFileIdsWhere(): " + sqlWhereClause, e);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Find and return list of files matching the specific Where clause.
   * Use findAllFilesWhere instead.  It returns a more generic data type
   *
   * @param sqlWhereClause a SQL where clause appropriate for the desired
   * files (do not begin the WHERE clause with the word WHERE!)
   * @return a list of FsContent each of which satisfy the given WHERE clause
   * @throws TskCoreException
   */
  @Deprecated  // use findAllFilesWhere instead
  public List<FsContent> findFilesWhere(String sqlWhereClause) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT * FROM tsk_files WHERE " + sqlWhereClause); //NON-NLS
      return resultSetToFsContents(rs);
    } catch (SQLException e) {
      throw new TskCoreException("SQLException thrown when calling 'SleuthkitCase.findFilesWhere().", e);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * @param dataSource the data source (Image, VirtualDirectory for file-sets,
   * etc) to search for the given file name
   * @param filePath The full path to the file(statement) of interest. This
   * can optionally include the image and volume names. Treated in a case-
   * insensitive manner.
   * @return a list of AbstractFile that have the given file path.
   */
  public List<AbstractFile> openFiles(Content dataSource, String filePath) throws TskCoreException {

    // get the non-unique path (strip of image and volume path segments, if
    // the exist.
    String path = AbstractFile.createNonUniquePath(filePath).toLowerCase();

    // split the file name from the parent path
    int lastSlash = path.lastIndexOf("/"); //NON-NLS

    // if the last slash is at the end, strip it off
    if (lastSlash == path.length()) {
      path = path.substring(0, lastSlash - 1);
      lastSlash = path.lastIndexOf("/"); //NON-NLS
    }

    String parentPath = path.substring(0, lastSlash);
    String fileName = path.substring(lastSlash);

    return findFiles(dataSource, fileName, parentPath);
  }

  /**
   * Get file layout ranges from tsk_file_layout, for a file with specified id
   *
   * @param id of the file to get file layout ranges for
   * @return list of populated file ranges
   * @throws TskCoreException thrown if a critical error occurred within tsk
   * core
   */
  public List<TskFileRange> getFileRanges(long id) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "select * from tsk_file_layout where obj_id = " + id + " order by sequence");
      List<TskFileRange> ranges = new ArrayList<TskFileRange>();
      while (rs.next()) {
        ranges.add(rsHelper.tskFileRange(rs));
      }
      return ranges;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting TskFileLayoutRanges by id, id = " + id, ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Get am image by the image object id
   *
   * @param id of the image object
   * @return Image object populated
   * @throws TskCoreException thrown if a critical error occurred within tsk
   * core
   */
  public Image getImageById(long id) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s1 = null;
    ResultSet rs1 = null;
    Statement s2 = null;
    ResultSet rs2 = null;
    try {
      s1 = connection.createStatement();
      rs1 = connection.executeQuery(s1, "SELECT * FROM tsk_image_info WHERE obj_id = " + id); //NON-NLS
      if (rs1.next()) {
        s2 = connection.createStatement();
        rs2 = connection.executeQuery(s2, "select * from tsk_image_names where obj_id = " + rs1.getLong("obj_id")); //NON-NLS
        List<String> imagePaths = new ArrayList<String>();
        while (rs2.next()) {
          imagePaths.add(rsHelper.imagePath(rs2));
        }
        return rsHelper.image(rs1, imagePaths.toArray(new String[imagePaths.size()]));
      } else {
        throw new TskCoreException("No image found for id: " + id);
      }
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting Image by id, id = " + id, ex);
    } finally {
      closeResultSet(rs2);
      closeStatement(s2);
      closeResultSet(rs1);
      closeStatement(s1);
      releaseSharedLock();
    }
  }

  /**
   * Get a volume system by the volume system object id
   *
   * @param id id of the volume system
   * @param parent image containing the volume system
   * @return populated VolumeSystem object
   * @throws TskCoreException thrown if a critical error occurred within tsk
   * core
   */
  VolumeSystem getVolumeSystemById(long id, Image parent) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "select * from tsk_vs_info " //NON-NLS
          + "where obj_id = " + id); //NON-NLS
      if (rs.next()) {
        return rsHelper.volumeSystem(rs, parent);
      } else {
        throw new TskCoreException("No volume system found for id:" + id);
      }
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting Volume System by ID.", ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * @param id ID of the desired VolumeSystem
   * @param parentId ID of the VolumeSystem'statement parent
   * @return the VolumeSystem with the given ID
   * @throws TskCoreException
   */
  VolumeSystem getVolumeSystemById(long id, long parentId) throws TskCoreException {
    VolumeSystem vs = getVolumeSystemById(id, null);
    vs.setParentId(parentId);
    return vs;
  }

  /**
   * Get a file system by the object id
   *
   * @param id of the filesystem
   * @param parent parent Image of the file system
   * @return populated FileSystem object
   * @throws TskCoreException thrown if a critical error occurred within tsk
   * core
   */
  FileSystem getFileSystemById(long id, Image parent) throws TskCoreException {
    return getFileSystemByIdHelper(id, parent);
  }

  /**
   * @param id ID of the desired FileSystem
   * @param parentId ID of the FileSystem'statement parent
   * @return the desired FileSystem
   * @throws TskCoreException
   */
  FileSystem getFileSystemById(long id, long parentId) throws TskCoreException {
    Volume vol = null;
    FileSystem fs = getFileSystemById(id, vol);
    fs.setParentId(parentId);
    return fs;
  }

  /**
   * Get a file system by the object id
   *
   * @param id of the filesystem
   * @param parent parent Volume of the file system
   * @return populated FileSystem object
   * @throws TskCoreException thrown if a critical error occurred within tsk
   * core
   */
  FileSystem getFileSystemById(long id, Volume parent) throws TskCoreException {
    return getFileSystemByIdHelper(id, parent);
  }

  /**
   * Get file system by id and Content parent
   *
   * @param id of the filesystem to get
   * @param parent a direct parent Content object
   * @return populated FileSystem object
   * @throws TskCoreException thrown if a critical error occurred within tsk
   * core
   */
  private FileSystem getFileSystemByIdHelper(long id, Content parent) throws TskCoreException {
    // see if we already have it
    // @@@ NOTE: this is currently kind of bad in that we are ignoring the parent value,
    // but it should be the same...
    synchronized (fileSystemIdMap) {
      if (fileSystemIdMap.containsKey(id)) {
        return fileSystemIdMap.get(id);
      }
    }
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "select * from tsk_fs_info " //NON-NLS
          + "where obj_id = " + id); //NON-NLS
      if (rs.next()) {
        FileSystem fs = rsHelper.fileSystem(rs, parent);
        // save it for the next call
        synchronized (fileSystemIdMap) {
          fileSystemIdMap.put(id, fs);
        }
        return fs;
      } else {
        throw new TskCoreException("No file system found for id:" + id);
      }
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting File System by ID", ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Get volume by id
   *
   * @param id
   * @param parent volume system
   * @return populated Volume object
   * @throws TskCoreException thrown if a critical error occurred within tsk
   * core
   */
  Volume getVolumeById(long id, VolumeSystem parent) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "select * from tsk_vs_parts " //NON-NLS
          + "where obj_id = " + id); //NON-NLS
      if (rs.next()) {
        return rsHelper.volume(rs, parent);
      } else {
        throw new TskCoreException("No volume found for id:" + id);
      }
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting Volume by ID", ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * @param id ID of the desired Volume
   * @param parentId ID of the Volume'statement parent
   * @return the desired Volume
   * @throws TskCoreException
   */
  Volume getVolumeById(long id, long parentId) throws TskCoreException {
    Volume vol = getVolumeById(id, null);
    vol.setParentId(parentId);
    return vol;
  }

  /**
   * Get a directory by id
   *
   * @param id of the directory object
   * @param parentFs parent file system
   * @return populated Directory object
   * @throws TskCoreException thrown if a critical error occurred within tsk
   * core
   */
  Directory getDirectoryById(long id, FileSystem parentFs) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT * FROM tsk_files " //NON-NLS
          + "WHERE obj_id = " + id);
      Directory temp = null; //NON-NLS
      if (rs.next()) {
        final short type = rs.getShort("type"); //NON-NLS
        if (type == TSK_DB_FILES_TYPE_ENUM.FS.getFileType()) {
          if (rs.getShort("meta_type") == TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_DIR.getValue()) { //NON-NLS
            temp = rsHelper.directory(rs, parentFs);
          }
        } else if (type == TSK_DB_FILES_TYPE_ENUM.VIRTUAL_DIR.getFileType()) {
          throw new TskCoreException("Expecting an FS-type directory, got virtual, id: " + id);
        }
      } else {
        throw new TskCoreException("No Directory found for id:" + id);
      }
      return temp;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting Directory by ID", ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Helper to return FileSystems in an Image
   *
   * @param image Image to lookup FileSystem for
   * @return Collection of FileSystems in the image
   */
  public Collection<FileSystem> getFileSystems(Image image) {
    List<FileSystem> fileSystems = new ArrayList<FileSystem>();
    CaseDbConnection connection;
    try {
      connection = connections.getConnection();
    } catch (TskCoreException ex) {
      logger.log(Level.SEVERE, "Error getting file systems for image " + image.getId(), ex); //NON-NLS     
      return fileSystems;
    }
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();

      // Get all the file systems.
      List<FileSystem> allFileSystems = new ArrayList<FileSystem>();
      try {
        rs = connection.executeQuery(s, "SELECT * FROM tsk_fs_info"); //NON-NLS
        while (rs.next()) {
          allFileSystems.add(rsHelper.fileSystem(rs, null));
        }
      } catch (SQLException ex) {
        logger.log(Level.SEVERE, "There was a problem while trying to obtain all file systems", ex); //NON-NLS
      } finally {
        closeResultSet(rs);
        rs = null;
      }

      // For each file system, find the image to which it belongs by iteratively
      // climbing the tsk_ojbects hierarchy only taking those file systems
      // that belong to this image.
      for (FileSystem fs : allFileSystems) {
        Long imageID = null;
        Long currentObjID = fs.getId();
        while (imageID == null) {
          try {
            rs = connection.executeQuery(s, "SELECT * FROM tsk_objects WHERE tsk_objects.obj_id = " + currentObjID); //NON-NLS
            currentObjID = rs.getLong("par_obj_id"); //NON-NLS
            if (rs.getInt("type") == TskData.ObjectType.IMG.getObjectType()) { //NON-NLS
              imageID = rs.getLong("obj_id"); //NON-NLS
            }
          } catch (SQLException ex) {
            logger.log(Level.SEVERE, "There was a problem while trying to obtain this image's file systems", ex); //NON-NLS
          } finally {
            closeResultSet(rs);
            rs = null;
          }
        }

        // see if imageID is this image'statement ID
        if (imageID == image.getId()) {
          fileSystems.add(fs);
        }
      }
    } catch (SQLException ex) {
      logger.log(Level.SEVERE, "Error getting case database connection", ex); //NON-NLS
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
    return fileSystems;
  }

  /**
   * Returns the list of direct children for a given Image
   *
   * @param img image to get children for
   * @return list of Contents (direct image children)
   * @throws TskCoreException thrown if a critical error occurred within tsk
   * core
   */
  List<Content> getImageChildren(Image img) throws TskCoreException {
    Collection<ObjectInfo> childInfos = getChildrenInfo(img);
    List<Content> children = new ArrayList<Content>();
    for (ObjectInfo info : childInfos) {
      if (info.type == ObjectType.VS) {
        children.add(getVolumeSystemById(info.id, img));
      } else if (info.type == ObjectType.FS) {
        children.add(getFileSystemById(info.id, img));
      } else if (info.type == ObjectType.ABSTRACTFILE) {
        children.add(getAbstractFileById(info.id));
      } else {
        throw new TskCoreException("Image has child of invalid type: " + info.type);
      }
    }
    return children;
  }

  /**
   * Returns the list of direct children IDs for a given Image
   *
   * @param img image to get children for
   * @return list of IDs (direct image children)
   * @throws TskCoreException thrown if a critical error occurred within tsk
   * core
   */
  List<Long> getImageChildrenIds(Image img) throws TskCoreException {
    Collection<ObjectInfo> childInfos = getChildrenInfo(img);
    List<Long> children = new ArrayList<Long>();
    for (ObjectInfo info : childInfos) {
      if (info.type == ObjectType.VS
          || info.type == ObjectType.FS
          || info.type == ObjectType.ABSTRACTFILE) {
        children.add(info.id);
      } else {
        throw new TskCoreException("Image has child of invalid type: " + info.type);
      }
    }
    return children;
  }

  /**
   * Returns the list of direct children for a given VolumeSystem
   *
   * @param vs volume system to get children for
   * @return list of volume system children objects
   * @throws TskCoreException thrown if a critical error occurred within tsk
   * core
   */
  List<Content> getVolumeSystemChildren(VolumeSystem vs) throws TskCoreException {
    Collection<ObjectInfo> childInfos = getChildrenInfo(vs);
    List<Content> children = new ArrayList<Content>();
    for (ObjectInfo info : childInfos) {
      if (info.type == ObjectType.VOL) {
        children.add(getVolumeById(info.id, vs));
      } else if (info.type == ObjectType.ABSTRACTFILE) {
        children.add(getAbstractFileById(info.id));
      } else {
        throw new TskCoreException("VolumeSystem has child of invalid type: " + info.type);
      }
    }
    return children;
  }

  /**
   * Returns the list of direct children IDs for a given VolumeSystem
   *
   * @param vs volume system to get children for
   * @return list of volume system children IDs
   * @throws TskCoreException thrown if a critical error occurred within tsk
   * core
   */
  List<Long> getVolumeSystemChildrenIds(VolumeSystem vs) throws TskCoreException {
    Collection<ObjectInfo> childInfos = getChildrenInfo(vs);
    List<Long> children = new ArrayList<Long>();
    for (ObjectInfo info : childInfos) {
      if (info.type == ObjectType.VOL || info.type == ObjectType.ABSTRACTFILE) {
        children.add(info.id);
      } else {
        throw new TskCoreException("VolumeSystem has child of invalid type: " + info.type);
      }
    }
    return children;
  }

  /**
   * Returns a list of direct children for a given Volume
   *
   * @param vol volume to get children of
   * @return list of Volume children
   * @throws TskCoreException thrown if a critical error occurred within tsk
   * core
   */
  List<Content> getVolumeChildren(Volume vol) throws TskCoreException {
    Collection<ObjectInfo> childInfos = getChildrenInfo(vol);
    List<Content> children = new ArrayList<Content>();
    for (ObjectInfo info : childInfos) {
      if (info.type == ObjectType.FS) {
        children.add(getFileSystemById(info.id, vol));
      } else if (info.type == ObjectType.ABSTRACTFILE) {
        children.add(getAbstractFileById(info.id));
      } else {
        throw new TskCoreException("Volume has child of invalid type: " + info.type);
      }
    }
    return children;
  }

  /**
   * Returns a list of direct children IDs for a given Volume
   *
   * @param vol volume to get children of
   * @return list of Volume children IDs
   * @throws TskCoreException thrown if a critical error occurred within tsk
   * core
   */
  List<Long> getVolumeChildrenIds(Volume vol) throws TskCoreException {
    final Collection<ObjectInfo> childInfos = getChildrenInfo(vol);
    final List<Long> children = new ArrayList<Long>();
    for (ObjectInfo info : childInfos) {
      if (info.type == ObjectType.FS || info.type == ObjectType.ABSTRACTFILE) {
        children.add(info.id);
      } else {
        throw new TskCoreException("Volume has child of invalid type: " + info.type);
      }
    }
    return children;
  }

  /**
   * Returns a map of image object IDs to a list of fully qualified file paths
   * for that image
   *
   * @return map of image object IDs to file paths
   * @throws TskCoreException thrown if a critical error occurred within tsk
   * core
   */
  public Map<Long, List<String>> getImagePaths() throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s1 = null;
    Statement s2 = null;
    ResultSet rs1 = null;
    ResultSet rs2 = null;
    try {
      s1 = connection.createStatement();
      rs1 = connection.executeQuery(s1, "select obj_id from tsk_image_info"); //NON-NLS
      s2 = connection.createStatement();
      Map<Long, List<String>> imgPaths = new LinkedHashMap<Long, List<String>>();
      while (rs1.next()) {
        long obj_id = rs1.getLong("obj_id"); //NON-NLS
        rs2 = connection.executeQuery(s2, "select * from tsk_image_names where obj_id = " + obj_id); //NON-NLS
        List<String> paths = new ArrayList<String>();
        while (rs2.next()) {
          paths.add(rsHelper.imagePath(rs2));
        }
        rs2.close();
        rs2 = null;
        imgPaths.put(obj_id, paths);
      }
      return imgPaths;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting image paths.", ex);
    } finally {
      closeResultSet(rs2);
      closeStatement(s2);
      closeResultSet(rs1);
      closeStatement(s1);
      releaseSharedLock();
    }
  }

  /**
   * @return a collection of Images associated with this instance of
   * SleuthkitCase
   * @throws TskCoreException
   */
  public List<Image> getImages() throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT obj_id FROM tsk_image_info"); //NON-NLS
      Collection<Long> imageIDs = new ArrayList<Long>();
      while (rs.next()) {
        imageIDs.add(rs.getLong("obj_id")); //NON-NLS
      }
      List<Image> images = new ArrayList<Image>();
      for (long id : imageIDs) {
        images.add(getImageById(id));
      }
      return images;
    } catch (SQLException ex) {
      throw new TskCoreException("Error retrieving images.", ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Get last (max) object id of content object in tsk_objects.
   *
   * @return currently max id
   * @throws TskCoreException exception thrown when database error occurs and
   * last object id could not be queried
   */
  public long getLastObjectId() throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireExclusiveLock();
    ResultSet rs = null;
    try {
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_MAX_OBJECT_ID);
      rs = connection.executeQuery(statement);
      long id = -1;
      if (rs.next()) {
        id = rs.getLong(1);
      }
      return id;
    } catch (SQLException e) {
      throw new TskCoreException("Error getting last object id", e);
    } finally {
      closeResultSet(rs);
      releaseExclusiveLock();
    }
  }

  /**
   * Set the file paths for the image given by obj_id
   *
   * @param obj_id the ID of the image to update
   * @param paths the fully qualified path to the files that make up the image
   * @throws TskCoreException exception thrown when critical error occurs
   * within tsk core and the update fails
   */
  public void setImagePaths(long obj_id, List<String> paths) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireExclusiveLock();
    Statement statement = null;
    try {
      connection.beginTransaction();
      statement = connection.createStatement();
      connection.executeUpdate(statement, "DELETE FROM tsk_image_names WHERE obj_id = " + obj_id); //NON-NLS
      for (int i = 0; i < paths.size(); i++) {
        connection.executeUpdate(statement, "INSERT INTO tsk_image_names VALUES (" + obj_id + ", \"" + paths.get(i) + "\", " + i + ")"); //NON-NLS
      }
      connection.commitTransaction();
    } catch (SQLException ex) {
      connection.rollbackTransaction();
      throw new TskCoreException("Error updating image paths.", ex);
    } finally {
      closeStatement(statement);
      releaseExclusiveLock();
    }
  }

  /**
   * Creates file object from a SQL query result set of rows from the
   * tsk_files table. Assumes that the query was of the form "SELECT * FROM
   * tsk_files WHERE XYZ".
   *
   * @param rs ResultSet to get content from. Caller is responsible for
   * closing it.
   * @return list of file objects from tsk_files table containing the results
   * @throws SQLException if the query fails
   */
  private List<AbstractFile> resultSetToAbstractFiles(ResultSet rs) throws SQLException {
    ArrayList<AbstractFile> results = new ArrayList<AbstractFile>();
    try {
      while (rs.next()) {
        final short type = rs.getShort("type"); //NON-NLS
        if (type == TSK_DB_FILES_TYPE_ENUM.FS.getFileType()) {
          FsContent result;
          if (rs.getShort("meta_type") == TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_DIR.getValue()) { //NON-NLS
            result = rsHelper.directory(rs, null);
          } else {
            result = rsHelper.file(rs, null);
          }
          results.add(result);
        } else if (type == TSK_DB_FILES_TYPE_ENUM.VIRTUAL_DIR.getFileType()) {
          final VirtualDirectory virtDir = rsHelper.virtualDirectory(rs);
          results.add(virtDir);
        } else if (type == TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS.getFileType()
            || type == TSK_DB_FILES_TYPE_ENUM.CARVED.getFileType()) {
          TSK_DB_FILES_TYPE_ENUM atype = TSK_DB_FILES_TYPE_ENUM.valueOf(type);
          String parentPath = rs.getString("parent_path"); //NON-NLS
          if (parentPath == null) {
            parentPath = ""; //NON-NLS
          }
          LayoutFile lf = new LayoutFile(this, rs.getLong("obj_id"), //NON-NLS
              rs.getString("name"), //NON-NLS
              atype,
              TSK_FS_NAME_TYPE_ENUM.valueOf(rs.getShort("dir_type")), TSK_FS_META_TYPE_ENUM.valueOf(rs.getShort("meta_type")), //NON-NLS
              TSK_FS_NAME_FLAG_ENUM.valueOf(rs.getShort("dir_flags")), rs.getShort("meta_flags"), //NON-NLS
              rs.getLong("size"), //NON-NLS
              rs.getString("md5"), FileKnown.valueOf(rs.getByte("known")), parentPath); //NON-NLS
          results.add(lf);
        } else if (type == TSK_DB_FILES_TYPE_ENUM.DERIVED.getFileType()) {
          final DerivedFile df;
          df = rsHelper.derivedFile(rs, AbstractContent.UNKNOWN_ID);
          results.add(df);
        } else if (type == TSK_DB_FILES_TYPE_ENUM.LOCAL.getFileType()) {
          final LocalFile lf;
          lf = rsHelper.localFile(rs, AbstractContent.UNKNOWN_ID);
          results.add(lf);
        }

      } //end for each resultSet
    } catch (SQLException e) {
      logger.log(Level.SEVERE, "Error getting abstract files from result set", e); //NON-NLS
    }

    return results;
  }

  /**
   * Creates FsContent objects from SQL query result set on tsk_files table
   *
   * @param rs the result set with the query results
   * @return list of fscontent objects matching the query
   * @throws SQLException if SQL query result getting failed
   */
  private List<FsContent> resultSetToFsContents(ResultSet rs) throws SQLException {
    List<FsContent> results = new ArrayList<FsContent>();
    List<AbstractFile> temp = resultSetToAbstractFiles(rs);
    for (AbstractFile f : temp) {
      final TSK_DB_FILES_TYPE_ENUM type = f.getType();
      if (type.equals(TskData.TSK_DB_FILES_TYPE_ENUM.FS)) {
        results.add((FsContent) f);
      }
    }
    return results;
  }

  /**
   * Process a read-only query on the tsk database, any table Can be used to
   * e.g. to find files of a given criteria. resultSetToFsContents() will
   * convert the results to useful objects. MUST CALL closeRunQuery() when
   * done
   *
   * @param query the given string query to run
   * @return  the resultSet from running the query. Caller MUST CALL
   * closeRunQuery(resultSet) as soon as possible, when done with retrieving
   * data from the resultSet
   * @throws SQLException if error occurred during the query
   * @deprecated use specific datamodel methods that encapsulate SQL layer
   */
  @Deprecated
  public ResultSet runQuery(String query) throws SQLException {
    CaseDbConnection connection;
    try {
      connection = connections.getConnection();
    } catch (TskCoreException ex) {
      throw new SQLException("Error getting connection for ad hoc query", ex);
    }
    acquireSharedLock();
    try {
      return connection.executeQuery(connection.createStatement(), query);
    } finally {
      //TODO unlock should be done in closeRunQuery()
      //but currently not all code calls closeRunQuery - need to fix this
      releaseSharedLock();
    }
  }

  /**
   * Closes ResultSet and its Statement previously retrieved from runQuery()
   *
   * @param resultSet with its Statement to close
   * @throws SQLException of closing the query results failed
   * @deprecated use specific datamodel methods that encapsulate SQL layer
   */
  @Deprecated
  public void closeRunQuery(ResultSet resultSet) throws SQLException {
    final Statement statement = resultSet.getStatement();
    resultSet.close();
    if (statement != null) {
      statement.close();
    }
  }

  @Override
  public void finalize() throws Throwable {
    try {
      close();
    } finally {
      super.finalize();
    }
  }

  /**
   * Call to free resources when done with instance.
   */
  public void close() {
    System.err.println(this.hashCode() + " closed"); //NON-NLS
    System.err.flush();

    fileSystemIdMap.clear();

    acquireExclusiveLock();
    try {
      if (this.caseHandle != null) {
        this.caseHandle.free();
        this.caseHandle = null;

      }
    } catch (TskCoreException ex) {
      logger.log(Level.WARNING,
          "Error freeing case handle.", ex); //NON-NLS
    } finally {
      releaseExclusiveLock();
    }
  }

  /**
   * Store the known status for the FsContent in the database Note: will not
   * update status if content is already 'Known Bad'
   *
   * @param  file  The AbstractFile object
   * @param  fileKnown  The object'statement known status
   * @return  true if the known status was updated, false otherwise
   * @throws TskCoreException thrown if a critical error occurred within tsk
   * core
   */
  public boolean setKnown(AbstractFile file, FileKnown fileKnown) throws TskCoreException {
    long id = file.getId();
    FileKnown currentKnown = file.getKnown();
    if (currentKnown.compareTo(fileKnown) > 0) {
      return false;
    }
    CaseDbConnection connection = connections.getConnection();
    acquireExclusiveLock();
    Statement statement = null;
    try {
      statement = connection.createStatement();
      connection.executeUpdate(statement, "UPDATE tsk_files " //NON-NLS
          + "SET known='" + fileKnown.getFileKnownValue() + "' " //NON-NLS
          + "WHERE obj_id=" + id); //NON-NLS
      file.setKnown(fileKnown);
    } catch (SQLException ex) {
      throw new TskCoreException("Error setting Known status.", ex);
    } finally {
      closeStatement(statement);
      releaseExclusiveLock();
    }
    return true;
  }

  /**
   * Store the md5Hash for the file in the database
   *
   * @param  file  The file object
   * @param  md5Hash  The object'statement md5Hash
   * @throws TskCoreException thrown if a critical error occurred within tsk
   * core
   */
  void setMd5Hash(AbstractFile file, String md5Hash) throws TskCoreException {
    long id = file.getId();
    CaseDbConnection connection = connections.getConnection();
    acquireExclusiveLock();
    try {
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.UPDATE_FILE_MD5);
      statement.clearParameters();
      statement.setString(1, md5Hash);
      statement.setLong(2, id);
      connection.executeUpdate(statement);
      file.setMd5Hash(md5Hash);
    } catch (SQLException ex) {
      throw new TskCoreException("Error setting MD5 hash", ex);
    } finally {
      releaseExclusiveLock();
    }
  }

  /**
   * Return the number of objects in the database of a given file type.
   *
   * @param contentType Type of file to count
   * @return Number of objects with that type.
   * @throws TskCoreException thrown if a critical error occurred within tsk
   * core
   */
  public int countFsContentType(TskData.TSK_FS_META_TYPE_ENUM contentType) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      Short contentShort = contentType.getValue();
      rs = connection.executeQuery(s, "SELECT COUNT(*) FROM tsk_files WHERE meta_type = '" + contentShort.toString() + "'"); //NON-NLS
      int count = 0;
      if (rs.next()) {
        count = rs.getInt(1);
      }
      return count;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting number of objects.", ex);
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Escape the single quotes in the given string so they can be added to the
   * SQL caseDbConnection
   *
   * @param text
   * @return text the escaped version
   */
  private static String escapeForBlackboard(String text) {
    if (text != null) {
      text = text.replaceAll("'", "''");
    }
    return text;
  }

  /**
   * Find all the files with the given MD5 hash.
   *
   * @param md5Hash hash value to match files with
   * @return List of AbstractFile with the given hash
   */
  public List<AbstractFile> findFilesByMd5(String md5Hash) {
    CaseDbConnection connection;
    try {
      connection = connections.getConnection();
    } catch (TskCoreException ex) {
      logger.log(Level.SEVERE, "Error finding files by md5 hash " + md5Hash, ex); //NON-NLS     
      return Collections.<AbstractFile>emptyList();
    }
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT * FROM tsk_files WHERE " //NON-NLS
          + " md5 = '" + md5Hash + "' " //NON-NLS
          + "AND size > 0"); //NON-NLS
      return resultSetToAbstractFiles(rs);
    } catch (SQLException ex) {
      logger.log(Level.WARNING, "Error querying database.", ex); //NON-NLS
      return Collections.<AbstractFile>emptyList();
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
  }

  /**
   * Query all the files to verify if they have an MD5 hash associated with
   * them.
   *
   * @return true if all files have an MD5 hash
   */
  public boolean allFilesMd5Hashed() {
    CaseDbConnection connection;
    try {
      connection = connections.getConnection();
    } catch (TskCoreException ex) {
      logger.log(Level.SEVERE, "Error checking md5 hashing status", ex); //NON-NLS     
      return false;
    }
    boolean allFilesAreHashed = false;
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT COUNT(*) FROM tsk_files " //NON-NLS
          + "WHERE dir_type = '" + TskData.TSK_FS_NAME_TYPE_ENUM.REG.getValue() + "' " //NON-NLS
          + "AND md5 IS NULL " //NON-NLS
          + "AND size > '0'"); //NON-NLS
      if (rs.next() && rs.getInt(1) == 0) {
        allFilesAreHashed = true;
      }
    } catch (SQLException ex) {
      logger.log(Level.WARNING, "Failed to query whether all files have MD5 hashes", ex); //NON-NLS
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
    return allFilesAreHashed;
  }

  /**
   * Query all the files and counts how many have an MD5 hash.
   *
   * @return the number of files with an MD5 hash
   */
  public int countFilesMd5Hashed() {
    CaseDbConnection connection;
    try {
      connection = connections.getConnection();
    } catch (TskCoreException ex) {
      logger.log(Level.SEVERE, "Error getting database connection for hashed files count", ex); //NON-NLS     
      return 0;
    }
    int count = 0;
    acquireSharedLock();
    Statement s = null;
    ResultSet rs = null;
    try {
      s = connection.createStatement();
      rs = connection.executeQuery(s, "SELECT COUNT(*) FROM tsk_files " //NON-NLS
          + "WHERE md5 IS NOT NULL " //NON-NLS
          + "AND size > '0'"); //NON-NLS
      if (rs.next()) {
        count = rs.getInt(1);
      }
    } catch (SQLException ex) {
      logger.log(Level.WARNING, "Failed to query for all the files.", ex); //NON-NLS
    } finally {
      closeResultSet(rs);
      closeStatement(s);
      releaseSharedLock();
    }
    return count;
  }

  /**
   * This is a temporary workaround to avoid an API change.
   *
   * @deprecated
   */
  @Deprecated
  public interface ErrorObserver {

    void receiveError(String context, String errorMessage);
  }

  /**
   * This is a temporary workaround to avoid an API change.
   *
   * @deprecated
   * @param observer The observer to add.
   */
  @Deprecated
  public void addErrorObserver(ErrorObserver observer) {
    errorObservers.add(observer);
  }

  /**
   * This is a temporary workaround to avoid an API change.
   *
   * @deprecated
   * @param observer The observer to remove.
   */
  @Deprecated
  public void removerErrorObserver(ErrorObserver observer) {
    int i = errorObservers.indexOf(observer);
    if (i >= 0) {
      errorObservers.remove(i);
    }
  }

  /**
   * This is a temporary workaround to avoid an API change.
   *
   * @deprecated
   * @param context The context in which the error occurred.
   * @param errorMessage A description of the error that occurred.
   */
  @Deprecated
  public void submitError(String context, String errorMessage) {
    for (ErrorObserver observer : errorObservers) {
      observer.receiveError(context, errorMessage);
    }
  }

  /**
   * Selects all of the rows from the tag_names table in the case database.
   *
   * @return A list, possibly empty, of TagName data transfer objects (DTOs)
   * for the rows.
   * @throws TskCoreException
   */
  public List<TagName> getAllTagNames() throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet resultSet = null;
    try {
      // SELECT * FROM tag_names
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_TAG_NAMES);
      resultSet = connection.executeQuery(statement);
      ArrayList<TagName> tagNames = new ArrayList<TagName>();
      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")))); //NON-NLS
      }
      return tagNames;
    } catch (SQLException ex) {
      throw new TskCoreException("Error selecting rows from tag_names table", ex);
    } finally {
      closeResultSet(resultSet);
      releaseSharedLock();
    }
  }

  /**
   * Selects all of the rows from the tag_names table in the case database for
   * which there is at least one matching row in the content_tags or
   * blackboard_artifact_tags tables.
   *
   * @return A list, possibly empty, of TagName data transfer objects (DTOs)
   * for the rows.
   * @throws TskCoreException
   */
  public List<TagName> getTagNamesInUse() throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet resultSet = null;
    try {
      // 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(CaseDbConnection.PREPARED_STATEMENT.SELECT_TAG_NAMES_IN_USE);
      resultSet = connection.executeQuery(statement);
      ArrayList<TagName> tagNames = new ArrayList<TagName>();
      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")))); //NON-NLS
      }
      return tagNames;
    } catch (SQLException ex) {
      throw new TskCoreException("Error selecting rows from tag_names table", ex);
    } finally {
      closeResultSet(resultSet);
      releaseSharedLock();
    }
  }

  /**
   * Inserts row into the tags_names table in the case database.
   *
   * @param displayName The display name for the new tag name.
   * @param description The description for the new tag name.
   * @param color The HTML color to associate with the new tag name.
   * @return A TagName data transfer object (DTO) for the new row.
   * @throws TskCoreException
   */
  public TagName addTagName(String displayName, String description, TagName.HTML_COLOR color) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireExclusiveLock();
    ResultSet resultSet = null;
    try {
      // INSERT INTO tag_names (display_name, description, color) VALUES (?, ?, ?)     
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.INSERT_TAG_NAME);
      statement.clearParameters();
      statement.setString(1, displayName);
      statement.setString(2, description);
      statement.setString(3, color.getName());
      connection.executeUpdate(statement);
      resultSet = statement.getGeneratedKeys();
      return new TagName(resultSet.getLong(1), displayName, description, color);
    } catch (SQLException ex) {
      throw new TskCoreException("Error adding row for " + displayName + " tag name to tag_names table", ex);
    } finally {
      closeResultSet(resultSet);
      releaseExclusiveLock();
    }
  }

  /**
   * 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 ContentTag addContentTag(Content content, TagName tagName, String comment, long beginByteOffset, long endByteOffset) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireExclusiveLock();
    ResultSet resultSet = null;
    try {
      // INSERT INTO content_tags (obj_id, tag_name_id, comment, begin_byte_offset, end_byte_offset) VALUES (?, ?, ?, ?, ?)
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.INSERT_CONTENT_TAG);
      statement.clearParameters();
      statement.setLong(1, content.getId());
      statement.setLong(2, tagName.getId());
      statement.setString(3, comment);
      statement.setLong(4, beginByteOffset);
      statement.setLong(5, endByteOffset);
      connection.executeUpdate(statement);
      resultSet = statement.getGeneratedKeys();
      return new ContentTag(resultSet.getLong(1), content, tagName, comment, beginByteOffset, endByteOffset);
    } 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);
      releaseExclusiveLock();
    }
  }

  /*
   * Deletes a row from the content_tags table in the case database.
   * @param tag A ContentTag data transfer object (DTO) for the row to delete.
   * @throws TskCoreException
   */
  public void deleteContentTag(ContentTag tag) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireExclusiveLock();
    try {
      // DELETE FROM content_tags WHERE tag_id = ?   
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.DELETE_CONTENT_TAG);
      statement.clearParameters();
      statement.setLong(1, tag.getId());
      connection.executeUpdate(statement);
    } catch (SQLException ex) {
      throw new TskCoreException("Error deleting row from content_tags table (id = " + tag.getId() + ")", ex);
    } finally {
      releaseExclusiveLock();
    }
  }

  /**
   * Selects all of the rows from the content_tags table in the case database.
   *
   * @return A list, possibly empty, of ContentTag data transfer objects
   * (DTOs) for the rows.
   * @throws TskCoreException
   */
  public List<ContentTag> getAllContentTags() throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet resultSet = null;
    try {
      // SELECT * FROM content_tags INNER JOIN tag_names ON content_tags.tag_name_id = tag_names.tag_name_id
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_CONTENT_TAGS);
      resultSet = connection.executeQuery(statement);
      ArrayList<ContentTag> tags = new ArrayList<ContentTag>();
      while (resultSet.next()) {
        TagName tagName = new TagName(resultSet.getLong(2), resultSet.getString("display_name"), resultSet.getString("description"), TagName.HTML_COLOR.getColorByName(resultSet.getString("color")))//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")))//NON-NLS
      }
      return tags;
    } catch (SQLException ex) {
      throw new TskCoreException("Error selecting rows from content_tags table", ex);
    } finally {
      closeResultSet(resultSet);
      releaseSharedLock();
    }
  }

  /**
   * Gets a count of the rows in the content_tags table in the case database
   * with a specified foreign key into the tag_names table.
   *
   * @param tagName A data transfer object (DTO) for the tag name to match.
   * @return The count, possibly zero.
   * @throws TskCoreException
   */
  public long getContentTagsCountByTagName(TagName tagName) throws TskCoreException {
    if (tagName.getId() == Tag.ID_NOT_SET) {
      throw new TskCoreException("TagName object is invalid, id not set");
    }
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet resultSet = null;
    try {
      // SELECT COUNT(*) FROM content_tags WHERE tag_name_id = ?
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.COUNT_CONTENT_TAGS_BY_TAG_NAME);
      statement.clearParameters();
      statement.setLong(1, tagName.getId());
      resultSet = connection.executeQuery(statement);
      if (resultSet.next()) {
        return resultSet.getLong(1);
      } else {
        throw new TskCoreException("Error getting content_tags row count for tag name (tag_name_id = " + tagName.getId() + ")");
      }
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting content_tags row count for tag name (tag_name_id = " + tagName.getId() + ")", ex);
    } finally {
      closeResultSet(resultSet);
      releaseSharedLock();
    }
  }

  /**
   * Selects the rows in the content_tags table in the case database with a
   * specified foreign key into the tag_names table.
   *
   * @param tagName A data transfer object (DTO) for the tag name to match.
   * @return A list, possibly empty, of ContentTag data transfer objects
   * (DTOs) for the rows.
   * @throws TskCoreException
   */
  public List<ContentTag> getContentTagsByTagName(TagName tagName) throws TskCoreException {
    if (tagName.getId() == Tag.ID_NOT_SET) {
      throw new TskCoreException("TagName object is invalid, id not set");
    }
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet resultSet = null;
    try {
      // SELECT * FROM content_tags WHERE tag_name_id = ?
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_CONTENT_TAGS_BY_TAG_NAME);
      statement.clearParameters();
      statement.setLong(1, tagName.getId());
      resultSet = connection.executeQuery(statement);
      ArrayList<ContentTag> tags = new ArrayList<ContentTag>();
      while (resultSet.next()) {
        ContentTag 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"))//NON-NLS
        tags.add(tag);
      }
      resultSet.close();
      return tags;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting content_tags rows (tag_name_id = " + tagName.getId() + ")", ex);
    } finally {
      closeResultSet(resultSet);
      releaseSharedLock();
    }
  }

  /**
   * Selects the rows in the content_tags table in the case database with a
   * specified foreign key into the tsk_objects table.
   *
   * @param content A data transfer object (DTO) for the content to match.
   * @return A list, possibly empty, of ContentTag data transfer objects
   * (DTOs) for the rows.
   * @throws TskCoreException
   */
  public List<ContentTag> getContentTagsByContent(Content content) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet resultSet = null;
    try {
      // SELECT * FROM content_tags INNER JOIN tag_names ON content_tags.tag_name_id = tag_names.tag_name_id WHERE content_tags.obj_id = ?
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_CONTENT_TAGS_BY_CONTENT);
      statement.clearParameters();
      statement.setLong(1, content.getId());
      resultSet = connection.executeQuery(statement);
      ArrayList<ContentTag> tags = new ArrayList<ContentTag>();
      while (resultSet.next()) {
        TagName tagName = new TagName(resultSet.getLong(2), resultSet.getString("display_name"), resultSet.getString("description"), TagName.HTML_COLOR.getColorByName(resultSet.getString("color")))//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"))//NON-NLS
        tags.add(tag);
      }
      return tags;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting content tags data for content (obj_id = " + content.getId() + ")", ex);
    } finally {
      closeResultSet(resultSet);
      releaseSharedLock();
    }
  }

  /**
   * 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 BlackboardArtifactTag addBlackboardArtifactTag(BlackboardArtifact artifact, TagName tagName, String comment) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireExclusiveLock();
    ResultSet resultSet = null;
    try {
      // INSERT INTO blackboard_artifact_tags (artifact_id, tag_name_id, comment, begin_byte_offset, end_byte_offset) VALUES (?, ?, ?, ?, ?)     
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.INSERT_ARTIFACT_TAG);
      statement.clearParameters();
      statement.setLong(1, artifact.getArtifactID());
      statement.setLong(2, tagName.getId());
      statement.setString(3, comment);
      connection.executeUpdate(statement);
      resultSet = statement.getGeneratedKeys();
      return new BlackboardArtifactTag(resultSet.getLong(1), artifact, getContentById(artifact.getObjectID()), tagName, comment);
    } 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);
      releaseExclusiveLock();
    }
  }

  /*
   * Deletes a row from the blackboard_artifact_tags table in the case database.
   * @param tag A BlackboardArtifactTag data transfer object (DTO) representing the row to delete.
   * @throws TskCoreException
   */
  public void deleteBlackboardArtifactTag(BlackboardArtifactTag tag) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireExclusiveLock();
    try {
      // DELETE FROM blackboard_artifact_tags WHERE tag_id = ?
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.DELETE_ARTIFACT_TAG);
      statement.clearParameters();
      statement.setLong(1, tag.getId());
      connection.executeUpdate(statement);
    } catch (SQLException ex) {
      throw new TskCoreException("Error deleting row from blackboard_artifact_tags table (id = " + tag.getId() + ")", ex);
    } finally {
      releaseExclusiveLock();
    }
  }

  /**
   * Selects all of the rows from the blackboard_artifacts_tags table in the
   * case database.
   *
   * @return A list, possibly empty, of BlackboardArtifactTag data transfer
   * objects (DTOs) for the rows.
   * @throws TskCoreException
   */
  public List<BlackboardArtifactTag> getAllBlackboardArtifactTags() throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet resultSet = null;
    try {
      // SELECT * FROM blackboard_artifact_tags INNER JOIN tag_names ON blackboard_artifact_tags.tag_name_id = tag_names.tag_name_id
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_ARTIFACT_TAGS);
      resultSet = connection.executeQuery(statement);
      ArrayList<BlackboardArtifactTag> tags = new ArrayList<BlackboardArtifactTag>();
      while (resultSet.next()) {
        TagName tagName = new TagName(resultSet.getLong(2), resultSet.getString("display_name"), resultSet.getString("description"), TagName.HTML_COLOR.getColorByName(resultSet.getString("color")))//NON-NLS
        BlackboardArtifact artifact = getBlackboardArtifact(resultSet.getLong("artifact_id")); //NON-NLS
        Content content = getContentById(artifact.getObjectID());
        BlackboardArtifactTag tag = new BlackboardArtifactTag(resultSet.getLong("tag_id"), artifact, content, tagName, resultSet.getString("comment"))//NON-NLS
        tags.add(tag);
      }
      return tags;
    } catch (SQLException ex) {
      throw new TskCoreException("Error selecting rows from blackboard_artifact_tags table", ex);
    } finally {
      closeResultSet(resultSet);
      releaseSharedLock();
    }
  }

  /**
   * Gets a count of the rows in the blackboard_artifact_tags table in the
   * case database with a specified foreign key into the tag_names table.
   *
   * @param tagName A data transfer object (DTO) for the tag name to match.
   * @return The count, possibly zero.
   * @throws TskCoreException
   */
  public long getBlackboardArtifactTagsCountByTagName(TagName tagName) throws TskCoreException {
    if (tagName.getId() == Tag.ID_NOT_SET) {
      throw new TskCoreException("TagName object is invalid, id not set");
    }
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet resultSet = null;
    try {
      // SELECT COUNT(*) FROM blackboard_artifact_tags WHERE tag_name_id = ?
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.COUNT_ARTIFACTS_BY_TAG_NAME);
      statement.clearParameters();
      statement.setLong(1, tagName.getId());
      resultSet = connection.executeQuery(statement);
      if (resultSet.next()) {
        return resultSet.getLong(1);
      } else {
        throw new TskCoreException("Error getting blackboard_artifact_tags row count for tag name (tag_name_id = " + tagName.getId() + ")");
      }
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting blackboard artifact_content_tags row count for tag name (tag_name_id = " + tagName.getId() + ")", ex);
    } finally {
      closeResultSet(resultSet);
      releaseSharedLock();
    }
  }

  /**
   * Selects the rows in the blackboard_artifacts_tags table in the case
   * database with a specified foreign key into the tag_names table.
   *
   * @param tagName A data transfer object (DTO) for the tag name to match.
   * @return A list, possibly empty, of BlackboardArtifactTag data transfer
   * objects (DTOs) for the rows.
   * @throws TskCoreException
   */
  public List<BlackboardArtifactTag> getBlackboardArtifactTagsByTagName(TagName tagName) throws TskCoreException {
    if (tagName.getId() == Tag.ID_NOT_SET) {
      throw new TskCoreException("TagName object is invalid, id not set");
    }
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet resultSet = null;
    try {
      // SELECT * FROM blackboard_artifact_tags WHERE tag_name_id = ?
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_ARTIFACT_TAGS_BY_TAG_NAME);
      statement.clearParameters();
      statement.setLong(1, tagName.getId());
      resultSet = connection.executeQuery(statement);
      ArrayList<BlackboardArtifactTag> tags = new ArrayList<BlackboardArtifactTag>();
      while (resultSet.next()) {
        BlackboardArtifact artifact = getBlackboardArtifact(resultSet.getLong("artifact_id")); //NON-NLS
        Content content = getContentById(artifact.getObjectID());
        BlackboardArtifactTag tag = new BlackboardArtifactTag(resultSet.getLong("tag_id"), artifact, content, tagName, resultSet.getString("comment"))//NON-NLS
        tags.add(tag);
      }
      return tags;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting blackboard artifact tags data (tag_name_id = " + tagName.getId() + ")", ex);
    } finally {
      closeResultSet(resultSet);
      releaseSharedLock();
    }
  }

  /**
   * Selects the rows in the blackboard_artifacts_tags table in the case
   * database with a specified foreign key into the blackboard_artifacts
   * table.
   *
   * @param artifact A data transfer object (DTO) for the artifact to match.
   * @return A list, possibly empty, of BlackboardArtifactTag data transfer
   * objects (DTOs) for the rows.
   * @throws TskCoreException
   */
  public List<BlackboardArtifactTag> getBlackboardArtifactTagsByArtifact(BlackboardArtifact artifact) throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet resultSet = null;
    try {
      // SELECT * FROM blackboard_artifact_tags INNER JOIN tag_names ON blackboard_artifact_tags.tag_name_id = tag_names.tag_name_id WHERE blackboard_artifact_tags.artifact_id = ?     
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_ARTIFACT_TAGS_BY_ARTIFACT);
      statement.clearParameters();
      statement.setLong(1, artifact.getArtifactID());
      resultSet = connection.executeQuery(statement);
      ArrayList<BlackboardArtifactTag> tags = new ArrayList<BlackboardArtifactTag>();
      while (resultSet.next()) {
        TagName tagName = new TagName(resultSet.getLong(2), resultSet.getString("display_name"), resultSet.getString("description"), TagName.HTML_COLOR.getColorByName(resultSet.getString("color")))//NON-NLS
        Content content = getContentById(artifact.getObjectID());
        BlackboardArtifactTag tag = new BlackboardArtifactTag(resultSet.getLong("tag_id"), artifact, content, tagName, resultSet.getString("comment"))//NON-NLS
        tags.add(tag);
      }
      return tags;
    } catch (SQLException ex) {
      throw new TskCoreException("Error getting blackboard artifact tags data (artifact_id = " + artifact.getArtifactID() + ")", ex);
    } finally {
      closeResultSet(resultSet);
      releaseSharedLock();
    }
  }

  /**
   * Inserts a row into the reports table in the case database.
   *
   * @param localPath The path of the report file, must be in the database
   * directory (case directory in Autopsy) or one of its subdirectories.
   * @param sourceModuleName The name of the module that created the report.
   * @param reportName The report name, may be empty.
   * @return A Report data transfer object (DTO) for the new row.
   * @throws TskCoreException
   */
  public Report addReport(String localPath, String sourceModuleName, String reportName) throws TskCoreException {
    // Make sure the local path of the report is in the database directory
    // or one of its subdirectories.
    String relativePath = ""; //NON-NLS
    try {
      relativePath = new File(getDbDirPath()).toURI().relativize(new File(localPath).toURI()).getPath();
    } catch (IllegalArgumentException ex) {
      String errorMessage = String.format("Local path %s not in the database directory or one of its subdirectories", localPath);
      throw new TskCoreException(errorMessage, ex);
    }

    // Figure out the create time of the report.
    long createTime = 0;
    try {
      java.io.File tempFile = new java.io.File(localPath);
      // Convert to UNIX epoch (seconds, not milliseconds).
      createTime = tempFile.lastModified() / 1000;
    } catch (Exception ex) {
      throw new TskCoreException("Could not get create time for report at " + localPath, ex);
    }

    // Write the report data to the database.
    CaseDbConnection connection = connections.getConnection();
    acquireExclusiveLock();
    ResultSet resultSet = null;
    try {
      // INSERT INTO reports (path, crtime, src_module_name, display_name) VALUES (?, ?, ?, ?)     
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.INSERT_REPORT);
      statement.clearParameters();
      statement.setString(1, relativePath);
      statement.setLong(2, createTime);
      statement.setString(3, sourceModuleName);
      statement.setString(4, reportName);
      connection.executeUpdate(statement);
      resultSet = statement.getGeneratedKeys();
      return new Report(resultSet.getLong(1), localPath, createTime, sourceModuleName, reportName);
    } catch (SQLException ex) {
      throw new TskCoreException("Error adding report " + localPath + " to reports table", ex);
    } finally {
      closeResultSet(resultSet);
      releaseExclusiveLock();
    }
  }

  /**
   * Selects all of the rows from the reports table in the case database.
   *
   * @return A list, possibly empty, of Report data transfer objects (DTOs)
   * for the rows.
   * @throws TskCoreException
   */
  public List<Report> getAllReports() throws TskCoreException {
    CaseDbConnection connection = connections.getConnection();
    acquireSharedLock();
    ResultSet resultSet = null;
    try {
      PreparedStatement statement = connection.getPreparedStatement(CaseDbConnection.PREPARED_STATEMENT.SELECT_REPORTS);
      resultSet = connection.executeQuery(statement);
      ArrayList<Report> reports = new ArrayList<Report>();
      while (resultSet.next()) {
        reports.add(new Report(resultSet.getLong("report_id"), //NON-NLS
            getDbDirPath() + java.io.File.separator + resultSet.getString("path"), //NON-NLS
            resultSet.getLong("crtime"), //NON-NLS
            resultSet.getString("src_module_name"), //NON-NLS
            resultSet.getString("report_name")))//NON-NLS
      }
      return reports;
    } catch (SQLException ex) {
      throw new TskCoreException("Error querying reports table", ex);
    } finally {
      closeResultSet(resultSet);
      releaseSharedLock();
    }
  }

  private static void closeResultSet(ResultSet resultSet) {
    if (resultSet != null) {
      try {
        resultSet.close();
      } catch (SQLException ex) {
        logger.log(Level.SEVERE, "Error closing ResultSet", ex); //NON-NLS
      }
    }
  }

  private static void closeStatement(Statement statement) {
    if (statement != null) {
      try {
        statement.close();
      } catch (SQLException ex) {
        logger.log(Level.SEVERE, "Error closing Statement", ex); //NON-NLS
      }
    }
  }

  /**
   * Provides thread confinement for connections to the underlying case
   * database. Note that the ThreadLocal base class releases its reference to
   * a per thread connection wrapper object to garbage collection when the
   * owning thread goes away, and we are relying on that garbage collection to
   * free JDBC resources - an override of finalize() by the wrapper failed to
   * close prepared statements because the connection is already closed. In
   * the future, we may wish to use a Connection pool instead.
   */
  private final class ConnectionPerThreadDispenser extends ThreadLocal<CaseDbConnection> {

    CaseDbConnection getConnection() throws TskCoreException {
      CaseDbConnection connection = get();
      if (!connection.isOpen()) {
        throw new TskCoreException("Case database connection for current thread is not open");
      }
      return connection;
    }

    @Override
    public CaseDbConnection initialValue() {
      return new CaseDbConnection(dbPath);
    }
  }

  /**
   * Encapsulates a connection to the underlying SQLite case database and a
   * set of prepared statements.
   */
  private static final class CaseDbConnection {

    enum PREPARED_STATEMENT {

      SELECT_ATTRIBUTES_OF_ARTIFACT("SELECT artifact_id, source, context, attribute_type_id, value_type, " //NON-NLS
          + "value_byte, value_text, value_int32, value_int64, value_double " //NON-NLS
          + "FROM blackboard_attributes WHERE artifact_id = ?"), //NON-NLS
      SELECT_ARTIFACT_BY_ID("SELECT obj_id, artifact_type_id FROM blackboard_artifacts WHERE artifact_id = ?"), //NON-NLS
      SELECT_ARTIFACTS_BY_TYPE("SELECT artifact_id, obj_id FROM blackboard_artifacts " //NON-NLS
          + "WHERE artifact_type_id = ?"), //NON-NLS
      COUNT_ARTIFACTS_OF_TYPE("SELECT COUNT(*) FROM blackboard_artifacts WHERE artifact_type_id = ?"), //NON-NLS
      COUNT_ARTIFACTS_FROM_SOURCE("SELECT COUNT(*) FROM blackboard_artifacts WHERE obj_id = ?"), //NON-NLS
      SELECT_ARTIFACTS_BY_SOURCE_AND_TYPE("SELECT artifact_id FROM blackboard_artifacts WHERE obj_id = ? AND artifact_type_id = ?"), //NON-NLS
      COUNT_ARTIFACTS_BY_SOURCE_AND_TYPE("SELECT COUNT(*) FROM blackboard_artifacts WHERE obj_id = ? AND artifact_type_id = ?"), //NON-NLS
      SELECT_FILES_BY_PARENT("SELECT tsk_files.* " //NON-NLS
          + "FROM tsk_objects INNER JOIN tsk_files " //NON-NLS
          + "ON tsk_objects.obj_id=tsk_files.obj_id " //NON-NLS
          + "WHERE (tsk_objects.par_obj_id = ? ) " //NON-NLS
          + "ORDER BY tsk_files.dir_type, tsk_files.name COLLATE NOCASE"), //NON-NLS
      SELECT_FILES_BY_PARENT_AND_TYPE("SELECT tsk_files.* " //NON-NLS
          + "FROM tsk_objects INNER JOIN tsk_files " //NON-NLS
          + "ON tsk_objects.obj_id=tsk_files.obj_id " //NON-NLS
          + "WHERE (tsk_objects.par_obj_id = ? AND tsk_files.type = ? ) " //NON-NLS
          + "ORDER BY tsk_files.dir_type, tsk_files.name COLLATE NOCASE"), //NON-NLS
      SELECT_FILE_IDS_BY_PARENT("SELECT tsk_files.obj_id FROM tsk_objects INNER JOIN tsk_files " //NON-NLS
          + "ON tsk_objects.obj_id=tsk_files.obj_id WHERE (tsk_objects.par_obj_id = ?)"), //NON-NLS
      SELECT_FILE_IDS_BY_PARENT_AND_TYPE("SELECT tsk_files.obj_id " //NON-NLS
          + "FROM tsk_objects INNER JOIN tsk_files " //NON-NLS
          + "ON tsk_objects.obj_id=tsk_files.obj_id " //NON-NLS
          + "WHERE (tsk_objects.par_obj_id = ? " //NON-NLS
          + "AND tsk_files.type = ? )"), //NON-NLS
      SELECT_FILE_BY_ID("SELECT * FROM tsk_files WHERE obj_id = ? LIMIT 1"), //NON-NLS
      INSERT_ARTIFACT("INSERT INTO blackboard_artifacts (artifact_id, obj_id, artifact_type_id) " //NON-NLS
          + "VALUES (NULL, ?, ?)"), //NON-NLS
      INSERT_STRING_ATTRIBUTE("INSERT INTO blackboard_attributes (artifact_id, artifact_type_id, source, context, attribute_type_id, value_type, value_text) " //NON-NLS
          + "VALUES (?,?,?,?,?,?,?)"), //NON-NLS
      INSERT_BYTE_ATTRIBUTE("INSERT INTO blackboard_attributes (artifact_id, artifact_type_id, source, context, attribute_type_id, value_type, value_byte) " //NON-NLS
          + "VALUES (?,?,?,?,?,?,?)"), //NON-NLS
      INSERT_INT_ATTRIBUTE("INSERT INTO blackboard_attributes (artifact_id, artifact_type_id, source, context, attribute_type_id, value_type, value_int32) " //NON-NLS
          + "VALUES (?,?,?,?,?,?,?)"), //NON-NLS
      INSERT_LONG_ATTRIBUTE("INSERT INTO blackboard_attributes (artifact_id, artifact_type_id, source, context, attribute_type_id, value_type, value_int64) " //NON-NLS
          + "VALUES (?,?,?,?,?,?,?)"), //NON-NLS
      INSERT_DOUBLE_ATTRIBUTE("INSERT INTO blackboard_attributes (artifact_id, artifact_type_id, source, context, attribute_type_id, value_type, value_double) " //NON-NLS
          + "VALUES (?,?,?,?,?,?,?)"), //NON-NLS
      SELECT_FILES_BY_FILE_SYSTEM_AND_NAME("SELECT * FROM tsk_files WHERE LOWER(name) LIKE ? and LOWER(name) NOT LIKE '%journal%' AND fs_obj_id = ?"), //NON-NLS
      SELECT_FILES_BY_FILE_SYSTEM_AND_PATH("SELECT * FROM tsk_files WHERE LOWER(name) LIKE ? AND LOWER(name) NOT LIKE '%journal%' AND LOWER(parent_path) LIKE ? AND fs_obj_id = ?"), //NON-NLS
      UPDATE_FILE_MD5("UPDATE tsk_files SET md5 = ? WHERE obj_id = ?"), //NON-NLS
      SELECT_LOCAL_PATH_FOR_FILE("SELECT path FROM tsk_files_path WHERE obj_id = ?"), //NON-NLS
      SELECT_PATH_FOR_FILE("SELECT parent_path FROM tsk_files WHERE obj_id = ?"), //NON-NLS
      SELECT_FILE_NAME("SELECT name FROM tsk_files WHERE obj_id = ?"), //NON-NLS
      SELECT_DERIVED_FILE("SELECT derived_id, rederive FROM tsk_files_derived WHERE obj_id = ?"), //NON-NLS
      SELECT_FILE_DERIVATION_METHOD("SELECT tool_name, tool_version, other FROM tsk_files_derived_method WHERE derived_id = ?"), //NON-NLS
      SELECT_MAX_OBJECT_ID("SELECT MAX(obj_id) from tsk_objects"), //NON-NLS
      INSERT_OBJECT("INSERT INTO tsk_objects (par_obj_id, type) VALUES (?, ?)"), //NON-NLS
      INSERT_FILE("INSERT INTO tsk_files (obj_id, fs_obj_id, name, type, has_path, dir_type, meta_type, dir_flags, meta_flags, size, ctime, crtime, atime, mtime, parent_path) " //NON-NLS
          + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"), //NON-NLS
      INSERT_LAYOUT_FILE("INSERT INTO tsk_file_layout (obj_id, byte_start, byte_len, sequence) " //NON-NLS
          + "VALUES (?, ?, ?, ?)"), //NON-NLS
      INSERT_LOCAL_PATH("INSERT INTO tsk_files_path (obj_id, path) VALUES (?, ?)"), //NON-NLS
      COUNT_CHILD_OBJECTS_BY_PARENT("SELECT COUNT(obj_id) FROM tsk_objects WHERE par_obj_id = ?"), //NON-NLS
      SELECT_FILE_SYSTEM_BY_OBJECT("SELECT fs_obj_id from tsk_files WHERE obj_id=?"), //NON-NLS
      SELECT_TAG_NAMES("SELECT * FROM tag_names"), //NON-NLS
      SELECT_TAG_NAMES_IN_USE("SELECT * FROM tag_names " //NON-NLS
          + "WHERE tag_name_id IN " //NON-NLS
          + "(SELECT tag_name_id from content_tags UNION SELECT tag_name_id FROM blackboard_artifact_tags)"), //NON-NLS
      INSERT_TAG_NAME("INSERT INTO tag_names (display_name, description, color) VALUES (?, ?, ?)"), //NON-NLS
      INSERT_CONTENT_TAG("INSERT INTO content_tags (obj_id, tag_name_id, comment, begin_byte_offset, end_byte_offset) VALUES (?, ?, ?, ?, ?)"), //NON-NLS
      DELETE_CONTENT_TAG("DELETE FROM content_tags WHERE tag_id = ?"), //NON-NLS
      COUNT_CONTENT_TAGS_BY_TAG_NAME("SELECT COUNT(*) FROM content_tags WHERE tag_name_id = ?"), //NON-NLS
      SELECT_CONTENT_TAGS("SELECT * FROM content_tags INNER JOIN tag_names ON content_tags.tag_name_id = tag_names.tag_name_id"), //NON-NLS
      SELECT_CONTENT_TAGS_BY_TAG_NAME("SELECT * FROM content_tags WHERE tag_name_id = ?"), //NON-NLS
      SELECT_CONTENT_TAGS_BY_CONTENT("SELECT * FROM content_tags INNER JOIN tag_names ON content_tags.tag_name_id = tag_names.tag_name_id WHERE content_tags.obj_id = ?"), //NON-NLS
      INSERT_ARTIFACT_TAG("INSERT INTO blackboard_artifact_tags (artifact_id, tag_name_id, comment) VALUES (?, ?, ?)"), //NON-NLS
      DELETE_ARTIFACT_TAG("DELETE FROM blackboard_artifact_tags WHERE tag_id = ?"), //NON-NLS
      SELECT_ARTIFACT_TAGS("SELECT * FROM blackboard_artifact_tags INNER JOIN tag_names ON blackboard_artifact_tags.tag_name_id = tag_names.tag_name_id"), //NON-NLS
      COUNT_ARTIFACTS_BY_TAG_NAME("SELECT COUNT(*) FROM blackboard_artifact_tags WHERE tag_name_id = ?"), //NON-NLS
      SELECT_ARTIFACT_TAGS_BY_TAG_NAME("SELECT * FROM blackboard_artifact_tags WHERE tag_name_id = ?"), //NON-NLS
      SELECT_ARTIFACT_TAGS_BY_ARTIFACT("SELECT * FROM blackboard_artifact_tags INNER JOIN tag_names ON blackboard_artifact_tags.tag_name_id = tag_names.tag_name_id WHERE blackboard_artifact_tags.artifact_id = ?"), //NON-NLS
      SELECT_REPORTS("SELECT * FROM reports"), //NON-NLS
      INSERT_REPORT("INSERT INTO reports (path, crtime, src_module_name, report_name) VALUES (?, ?, ?, ?)");   //NON-NLS

      private final String sql;

      private PREPARED_STATEMENT(String sql) {
        this.sql = sql;
      }

      String getSQL() {
        return sql;
      }
    }
    private final Map<PREPARED_STATEMENT, PreparedStatement> preparedStatements;
    private Connection connection;

    CaseDbConnection(String dbPath) {
      this.preparedStatements = new EnumMap<PREPARED_STATEMENT, PreparedStatement>(PREPARED_STATEMENT.class);
      Statement statement = null;
      try {
        this.connection = DriverManager.getConnection("jdbc:sqlite:" + dbPath); //NON-NLS
        statement = createStatement();
        statement.execute("PRAGMA synchronous = OFF;"); // Reduce I/O operations, we have no OS crash recovery anyway. //NON-NLS     
        statement.execute("PRAGMA read_uncommitted = True;"); // Allow query while in transaction. //NON-NLS
        statement.execute("PRAGMA foreign_keys = ON;"); // Enforce foreign key constraints. //NON-NLS
      } catch (SQLException ex) {
        // The exception is caught and logged here because this
        // constructor will be called by an override of
        // ThreadLocal<T>.initialValue() which cannot throw. Calls to
        // ConnectionPerThreadDispenser.getConnection() will detect
        // the error state via isOpen() and throw an appropriate
        // exception.
        SleuthkitCase.logger.log(Level.SEVERE, "Error setting up case database connection for thread", ex); //NON-NLS
        if (this.connection != null) {
          try {
            this.connection.close();
          } catch (SQLException e) {
            SleuthkitCase.logger.log(Level.SEVERE, "Failed to close connection", e);
          }
          this.connection = null;
        }
      } finally {
        closeStatement(statement);
      }
    }

    boolean isOpen() {
      return this.connection != null;
    }

    PreparedStatement getPreparedStatement(PREPARED_STATEMENT statementKey) throws SQLException {
      // Lazy statement preparation.
      PreparedStatement statement;
      if (this.preparedStatements.containsKey(statementKey)) {
        statement = this.preparedStatements.get(statementKey);
      } else {
        statement = prepareStatement(statementKey.getSQL());
        this.preparedStatements.put(statementKey, statement);
      }
      return statement;
    }

    private PreparedStatement prepareStatement(String sqlStatement) throws SQLException {
      PreparedStatement statement = null;
      boolean locked = true;
      while (locked) {
        try {
          statement = this.connection.prepareStatement(sqlStatement);
          locked = false;
        } catch (SQLException ex) {
          if (ex.getErrorCode() != SQLITE_BUSY_ERROR && ex.getErrorCode() != DATABASE_LOCKED_ERROR) {
            throw ex;
          }
        }
      }
      return statement;
    }

    Statement createStatement() throws SQLException {
      Statement statement = null;
      boolean locked = true;
      while (locked) {
        try {
          statement = this.connection.createStatement();
          locked = false;
        } catch (SQLException ex) {
          if (ex.getErrorCode() != SQLITE_BUSY_ERROR && ex.getErrorCode() != DATABASE_LOCKED_ERROR) {
            throw ex;
          }
        }
      }
      return statement;
    }

    void beginTransaction() throws SQLException {
      boolean locked = true;
      while (locked) {
        try {
          connection.setAutoCommit(false);
          locked = false;
        } catch (SQLException ex) {
          if (ex.getErrorCode() != SQLITE_BUSY_ERROR && ex.getErrorCode() != DATABASE_LOCKED_ERROR) {
            throw ex;
          }
        }
      }
    }

    void commitTransaction() throws SQLException {
      try {
        connection.commit();
      } finally {
        connection.setAutoCommit(true);
      }
    }

    /**
     * A rollback that logs exceptions and does not throw, intended for
     * "internal" use in SleuthkitCase methods where the exception that
     * motivated the rollback is the exception to report to the client.
     */
    void rollbackTransaction() {
      try {
        connection.rollback();
      } catch (SQLException e) {
        logger.log(Level.SEVERE, "Error rolling back transaction", e);
      }
      try {
        connection.setAutoCommit(true);
      } catch (SQLException e) {
        logger.log(Level.SEVERE, "Error restoring auto-commit", e);
      }
    }

    /**
     * A rollback that throws, intended for use by the CaseDbTransaction
     * class where client code is managing the transaction and the client
     * may wish to know that the rollback failed.
     *
     * @throws SQLException
     */
    void rollbackTransactionWithThrow() throws SQLException {
      try {
        connection.rollback();
      } finally {
        connection.setAutoCommit(true);
      }
    }

    private ResultSet executeQuery(Statement statement, String query) throws SQLException {
      ResultSet resultSet = null;
      boolean locked = true;
      while (locked) {
        try {
          resultSet = statement.executeQuery(query);
          locked = false;
        } catch (SQLException ex) {
          if (ex.getErrorCode() != SQLITE_BUSY_ERROR && ex.getErrorCode() != DATABASE_LOCKED_ERROR) {
            throw ex;
          }
        }
      }
      return resultSet;
    }

    private ResultSet executeQuery(PreparedStatement statement) throws SQLException {
      ResultSet resultSet = null;
      boolean locked = true;
      while (locked) {
        try {
          resultSet = statement.executeQuery();
          locked = false;
        } catch (SQLException ex) {
          if (ex.getErrorCode() != SQLITE_BUSY_ERROR && ex.getErrorCode() != DATABASE_LOCKED_ERROR) {
            throw ex;
          }
        }
      }
      return resultSet;
    }

    void executeUpdate(Statement statement, String update) throws SQLException {
      boolean locked = true;
      while (locked) {
        try {
          statement.executeUpdate(update);
          locked = false;
        } catch (SQLException ex) {
          if (ex.getErrorCode() != SQLITE_BUSY_ERROR && ex.getErrorCode() != DATABASE_LOCKED_ERROR) {
            throw ex;
          }
        }
      }
    }

    void executeUpdate(PreparedStatement statement) throws SQLException {
      boolean locked = true;
      while (locked) {
        try {
          statement.executeUpdate();
          locked = false;
        } catch (SQLException ex) {
          if (ex.getErrorCode() != SQLITE_BUSY_ERROR && ex.getErrorCode() != DATABASE_LOCKED_ERROR) {
            throw ex;
          }
        }
      }
    }
  }

  /**
   * Wraps the transactional capabilities of a CaseDbConnection object to
   * support use cases where control of a transaction is given to a
   * SleuthkitCase client. Note that this class does not implement the
   * Transaction interface because that sort of flexibility and its associated
   * complexity is not needed. Also, TskCoreExceptions are thrown to be
   * consistent with the outer SleuthkitCase class.
   */
  public static final class CaseDbTransaction {

    private final CaseDbConnection connection;

    private CaseDbTransaction(CaseDbConnection connection) throws TskCoreException {
      this.connection = connection;
      try {
        this.connection.beginTransaction();
      } catch (SQLException ex) {
        throw new TskCoreException("Failed to create transaction on case database", ex);
      }
    }

    /**
     * The implementations of the public APIs that take a CaseDbTransaction
     * object need access to the underlying CaseDbConnection.
     *
     * @return The CaseDbConnection instance for this instance of
     * CaseDbTransaction.
     */
    private CaseDbConnection getConnection() {
      return this.connection;
    }

    /**
     * Commits the transaction on the case database that was begun when this
     * object was constructed.
     *
     * @throws TskCoreException
     */
    public void commit() throws TskCoreException {
      try {
        this.connection.commitTransaction();
      } catch (SQLException ex) {
        throw new TskCoreException("Failed to commit transaction on case database", ex);
      }
    }

    /**
     * Rolls back the transaction on the case database that was begun when
     * this object was constructed.
     *
     * @throws TskCoreException
     */
    public void rollback() throws TskCoreException {
      try {
        this.connection.rollbackTransactionWithThrow();
      } catch (SQLException ex) {
        throw new TskCoreException("Case database transaction rollback failed", ex);
      }
    }
  }
}
TOP

Related Classes of org.sleuthkit.datamodel.SleuthkitCase$CaseDbTransaction

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.