blob: 44755043b3998f7f0b73d23594701ed332d40986 [file] [log] [blame]
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<SQLiteClosable, Object> 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<Object> 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<Object> 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();
}
}
}