package com.xtremelabs.robolectric.shadows; import android.content.ContentValues; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.*; import com.xtremelabs.robolectric.Robolectric; import com.xtremelabs.robolectric.internal.Implementation; import com.xtremelabs.robolectric.internal.Implements; import com.xtremelabs.robolectric.internal.RealObject; import com.xtremelabs.robolectric.util.DatabaseConfig; import com.xtremelabs.robolectric.util.SQLite.SQLStringAndBindings; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Iterator; import java.util.WeakHashMap; import java.util.concurrent.locks.ReentrantLock; import static com.xtremelabs.robolectric.Robolectric.newInstanceOf; import static com.xtremelabs.robolectric.Robolectric.shadowOf; import static com.xtremelabs.robolectric.util.SQLite.buildDeleteString; import static com.xtremelabs.robolectric.util.SQLite.buildInsertString; import static com.xtremelabs.robolectric.util.SQLite.buildUpdateString; import static com.xtremelabs.robolectric.util.SQLite.buildWhereClause; /** * Shadow for {@code SQLiteDatabase} that simulates the movement of a {@code Cursor} through database tables. * Implemented as a wrapper around an embedded SQL database, accessed via JDBC. The JDBC connection is * made available to test cases for use in fixture setup and assertions. */ @Implements(SQLiteDatabase.class) public class ShadowSQLiteDatabase { @RealObject SQLiteDatabase realSQLiteDatabase; private static Connection connection; private final ReentrantLock mLock = new ReentrantLock(true); private boolean mLockingEnabled = true; private WeakHashMap mPrograms; private boolean inTransaction = false; private boolean transactionSuccess = false; private boolean throwOnInsert; @Implementation public void setLockingEnabled(boolean lockingEnabled) { mLockingEnabled = lockingEnabled; } public void lock() { if (!mLockingEnabled) return; mLock.lock(); } public void unlock() { if (!mLockingEnabled) return; mLock.unlock(); } public void setThrowOnInsert(boolean throwOnInsert) { this.throwOnInsert = throwOnInsert; } @Implementation public static SQLiteDatabase openDatabase(String path, SQLiteDatabase.CursorFactory factory, int flags) { connection = DatabaseConfig.getMemoryConnection(); return newInstanceOf(SQLiteDatabase.class); } @Implementation public long insert(String table, String nullColumnHack, ContentValues values) { return insertWithOnConflict(table, nullColumnHack, values, SQLiteDatabase.CONFLICT_NONE); } @Implementation public long insertOrThrow(String table, String nullColumnHack, ContentValues values) { if (throwOnInsert) throw new android.database.SQLException(); return insertWithOnConflict(table, nullColumnHack, values, SQLiteDatabase.CONFLICT_NONE); } @Implementation public long replace(String table, String nullColumnHack, ContentValues values) { return insertWithOnConflict(table, nullColumnHack, values, SQLiteDatabase.CONFLICT_REPLACE); } @Implementation public long insertWithOnConflict(String table, String nullColumnHack, ContentValues initialValues, int conflictAlgorithm) { try { SQLStringAndBindings sqlInsertString = buildInsertString(table, initialValues, conflictAlgorithm); PreparedStatement insert = connection.prepareStatement(sqlInsertString.sql, Statement.RETURN_GENERATED_KEYS); Iterator columns = sqlInsertString.columnValues.iterator(); int i = 1; long result = -1; while (columns.hasNext()) { insert.setObject(i++, columns.next()); } insert.executeUpdate(); ResultSet resultSet = insert.getGeneratedKeys(); if (resultSet.next()) { result = resultSet.getLong(1); } resultSet.close(); return result; } catch (SQLException e) { return -1; // this is how SQLite behaves, unlike H2 which throws exceptions } } @Implementation public Cursor query(boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) { String where = selection; if (selection != null && selectionArgs != null) { where = buildWhereClause(selection, selectionArgs); } String sql = SQLiteQueryBuilder.buildQueryString(distinct, table, columns, where, groupBy, having, orderBy, limit); ResultSet resultSet; try { Statement statement = connection.createStatement(DatabaseConfig.getResultSetType(), ResultSet.CONCUR_READ_ONLY); resultSet = statement.executeQuery(sql); } catch (SQLException e) { throw new RuntimeException("SQL exception in query", e); } SQLiteCursor cursor = new SQLiteCursor(null, null, null, null); shadowOf(cursor).setResultSet(resultSet,sql); return cursor; } @Implementation public Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy) { return query(false, table, columns, selection, selectionArgs, groupBy, having, orderBy, null); } @Implementation public Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) { return query(false, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit); } @Implementation public int update(String table, ContentValues values, String whereClause, String[] whereArgs) { SQLStringAndBindings sqlUpdateString = buildUpdateString(table, values, whereClause, whereArgs); try { PreparedStatement statement = connection.prepareStatement(sqlUpdateString.sql); Iterator columns = sqlUpdateString.columnValues.iterator(); int i = 1; while (columns.hasNext()) { statement.setObject(i++, columns.next()); } return statement.executeUpdate(); } catch (SQLException e) { throw new RuntimeException("SQL exception in update", e); } } @Implementation public int delete(String table, String whereClause, String[] whereArgs) { String sql = buildDeleteString(table, whereClause, whereArgs); try { return connection.prepareStatement(sql).executeUpdate(); } catch (SQLException e) { throw new RuntimeException("SQL exception in delete", e); } } @Implementation public void execSQL(String sql) throws android.database.SQLException { if (!isOpen()) { throw new IllegalStateException("database not open"); } try { String scrubbedSql= DatabaseConfig.getScrubSQL(sql); connection.createStatement().execute(scrubbedSql); } catch (java.sql.SQLException e) { android.database.SQLException ase = new android.database.SQLException(); ase.initCause(e); throw ase; } } @Implementation public void execSQL(String sql, Object[] bindArgs) throws SQLException { if (bindArgs == null) { throw new IllegalArgumentException("Empty bindArgs"); } String scrubbedSql= DatabaseConfig.getScrubSQL(sql); SQLiteStatement statement = null; try { statement =compileStatement(scrubbedSql); if (bindArgs != null) { int numArgs = bindArgs.length; for (int i = 0; i < numArgs; i++) { DatabaseUtils.bindObjectToProgram(statement, i + 1, bindArgs[i]); } } statement.execute(); } catch (SQLiteDatabaseCorruptException e) { throw e; } finally { if (statement != null) { statement.close(); } } } @Implementation public Cursor rawQuery (String sql, String[] selectionArgs) { return rawQueryWithFactory( new SQLiteDatabase.CursorFactory() { @Override public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery, String editTable, SQLiteQuery query) { return new SQLiteCursor(db, masterQuery, editTable, query); } }, sql, selectionArgs, null ); } @Implementation public Cursor rawQueryWithFactory (SQLiteDatabase.CursorFactory cursorFactory, String sql, String[] selectionArgs, String editTable) { String sqlBody = sql; if (sql != null) { sqlBody = buildWhereClause(sql, selectionArgs); } ResultSet resultSet; try { SQLiteStatement stmt = compileStatement(sql); int numArgs = selectionArgs == null ? 0 : selectionArgs.length; for (int i = 0; i < numArgs; i++) { stmt.bindString(i + 1, selectionArgs[i]); } resultSet = Robolectric.shadowOf(stmt).getStatement().executeQuery(); } catch (SQLException e) { throw new RuntimeException("SQL exception in query", e); } //TODO: assert rawquery with args returns actual values SQLiteCursor cursor = (SQLiteCursor) cursorFactory.newCursor(null, null, null, null); shadowOf(cursor).setResultSet(resultSet, sqlBody); return cursor; } @Implementation public boolean isOpen() { return (connection != null); } @Implementation public void close() { if (!isOpen()) { return; } try { connection.close(); connection = null; } catch (SQLException e) { throw new RuntimeException("SQL exception in close", e); } } @Implementation public void beginTransaction() { try { connection.setAutoCommit(false); } catch (SQLException e) { throw new RuntimeException("SQL exception in beginTransaction", e); } finally { inTransaction = true; } } @Implementation public void setTransactionSuccessful() { if (!isOpen()) { throw new IllegalStateException("connection is not opened"); } else if (transactionSuccess) { throw new IllegalStateException("transaction already successfully"); } transactionSuccess = true; } @Implementation public void endTransaction() { try { if (transactionSuccess) { transactionSuccess = false; connection.commit(); } else { connection.rollback(); } connection.setAutoCommit(true); } catch (SQLException e) { throw new RuntimeException("SQL exception in beginTransaction", e); } finally { inTransaction = false; } } @Implementation public boolean inTransaction() { return inTransaction; } /** * Allows tests cases to query the transaction state * @return */ public boolean isTransactionSuccess() { return transactionSuccess; } /** * Allows test cases access to the underlying JDBC connection, for use in * setup or assertions. * * @return the connection */ public Connection getConnection() { return connection; } @Implementation public SQLiteStatement compileStatement(String sql) throws SQLException { lock(); String scrubbedSql= DatabaseConfig.getScrubSQL(sql); try { SQLiteStatement stmt = Robolectric.newInstanceOf(SQLiteStatement.class); Robolectric.shadowOf(stmt).init(realSQLiteDatabase, scrubbedSql); return stmt; } catch (Exception e){ throw new RuntimeException(e); } finally { unlock(); } } /** * @param closable */ void addSQLiteClosable(SQLiteClosable closable) { lock(); try { mPrograms.put(closable, null); } finally { unlock(); } } void removeSQLiteClosable(SQLiteClosable closable) { lock(); try { mPrograms.remove(closable); } finally { unlock(); } } }