diff --git a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java index 8079e6d5af77d5b0b039056bfe3479385bda5313..9b4de4ebc4ac8dbc96550360be5f57853a500d14 100644 --- a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java +++ b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java @@ -48,6 +48,7 @@ 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.SQLiteConfig; import org.sqlite.SQLiteJDBCLoader; /** @@ -4098,6 +4099,19 @@ public void closeRunQuery(ResultSet resultSet) throws SQLException { } } + /** + * This method allows developers to run arbitrary SQL "SELECT" + * queries. The CaseDbQuery object will take care to acquiring + * the necessary database lock and when used in a try-with-resources + * block will automatically take care of releasing the lock. + * @param query The query string to execute. + * @return A CaseDbQuery instance. + * @throws TskCoreException + */ + public CaseDbQuery executeQuery(String query) throws TskCoreException { + return new CaseDbQuery(query); + } + @Override public void finalize() throws Throwable { try { @@ -4112,8 +4126,8 @@ public void finalize() throws Throwable { */ public void close() { System.err.println(this.hashCode() + " closed"); //NON-NLS - acquireExclusiveLock(); System.err.flush(); + acquireExclusiveLock(); connections.close(); fileSystemIdMap.clear(); @@ -5110,11 +5124,22 @@ String getSQL() { 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 + SQLiteConfig config = new SQLiteConfig(); + + // Reduce I/O operations, we have no OS crash recovery anyway. + config.setSynchronous(SQLiteConfig.SynchronousMode.OFF); + + // The original comment for "read_uncommited" indicating that it + // was being set to "allow query while in transaction". I don't fully + // understand why this is needed since all it does it expose dirty writes + // within one transaction to other queries. There was also the suggestion + // that it may have helped to increase performance. + config.setReadUncommited(true); + + // Enforce foreign key constraints. + config.enforceForeignKeys(true); + + this.connection = DriverManager.getConnection("jdbc:sqlite:" + dbPath, config.toProperties()); //NON-NLS } catch (SQLException ex) { // The exception is caught and logged here because this // constructor will be called by an override of @@ -5131,8 +5156,6 @@ String getSQL() { } this.connection = null; } - } finally { - closeStatement(statement); } } @@ -5199,11 +5222,26 @@ void beginTransaction() throws SQLException { } void commitTransaction() throws SQLException { + boolean locked = true; + + // Exceptions can be thrown on a call to commit so we will retry + // until it succeeds. + while (locked) { + try { + connection.commit(); + locked = false; + } catch (SQLException ex) { + logger.log(Level.SEVERE, String.format("Exception commiting transaction: Error code: %d SQLState: %s", ex.getErrorCode(), ex.getSQLState()), ex); + } + } + + // You must turn auto commit back on when done with the transaction. try { - connection.commit(); - } finally { connection.setAutoCommit(true); } + catch (SQLException ex) { + logger.log(Level.SEVERE, String.format("Exception resetting auto commit: Error code: %d SQLState: %s", ex.getErrorCode(), ex.getSQLState()), ex); + } } /** @@ -5373,4 +5411,69 @@ public void rollback() throws TskCoreException { } } } + + /** + * The CaseDbQuery supports the use case where developers have a + * need for data that is not exposed through the SleuthkitCase API. + * A CaseDbQuery instance gets created through the SleuthkitCase + * executeDbQuery() method. It wraps the ResultSet and takes care + * of acquiring and releasing the appropriate database lock. + * It implements AutoCloseable so that it can be used in a try-with + * -resources block freeing developers from having to remember to + * close the result set and releasing the lock. + * + */ + public final class CaseDbQuery implements AutoCloseable { + private ResultSet resultSet; + + private CaseDbQuery(String query) throws TskCoreException { + if (!query.regionMatches(true, 0, "SELECT", 0, "SELECT".length())) { + throw new TskCoreException("Unsupported query: Only SELECT queries are supported."); + } + + CaseDbConnection connection; + + try { + connection = connections.getConnection(); + } catch (TskCoreException ex) { + throw new TskCoreException("Error getting connection for query: ", ex); + } + + try { + SleuthkitCase.this.acquireSharedLock(); + resultSet = connection.executeQuery(connection.createStatement(), query); + } + catch (SQLException ex) + { + SleuthkitCase.this.releaseSharedLock(); + throw new TskCoreException("Error executing query: ", ex); + } + } + + /** + * Get the result set for this query. + * @return The result set. + */ + public ResultSet getResultSet() { + return resultSet; + } + + @Override + public void close() throws TskCoreException { + try { + if (resultSet != null) { + final Statement statement = resultSet.getStatement(); + if (statement != null) { + statement.close(); + } + resultSet.close(); + } + + SleuthkitCase.this.releaseSharedLock(); + } + catch (SQLException ex) { + throw new TskCoreException("Error closing query: ", ex); + } + } + } }