blob: 65ba450a3e2b6df7973e06b41d2fe0755172d53c [file] [log] [blame]
package org.robolectric.shadows;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteClosable;
import android.database.sqlite.SQLiteCursor;
import android.database.sqlite.SQLiteCursorDriver;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabaseCorruptException;
import android.database.sqlite.SQLiteQuery;
import android.database.sqlite.SQLiteQueryBuilder;
import android.database.sqlite.SQLiteStatement;
import org.robolectric.Robolectric;
import org.robolectric.internal.Implementation;
import org.robolectric.internal.Implements;
import org.robolectric.internal.RealObject;
import org.robolectric.util.DatabaseConfig;
import org.robolectric.util.SQLite.*;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.locks.ReentrantLock;
import static org.robolectric.Robolectric.newInstanceOf;
import static org.robolectric.Robolectric.shadowOf;
import static org.robolectric.util.SQLite.*;
/**
* 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(value = SQLiteDatabase.class, inheritImplementationMethods = true)
public class ShadowSQLiteDatabase extends ShadowSQLiteClosable {
public static final android.database.sqlite.SQLiteDatabase.CursorFactory DEFAULT_CURSOR_FACTORY = new SQLiteDatabase.CursorFactory() {
@Override
public Cursor newCursor(SQLiteDatabase db,
SQLiteCursorDriver masterQuery, String editTable, SQLiteQuery query) {
return new SQLiteCursor(db, masterQuery, editTable, query);
}
};
@RealObject SQLiteDatabase realSQLiteDatabase;
private static Connection connection;
private final ReentrantLock mLock = new ReentrantLock(true);
private boolean mLockingEnabled = true;
private WeakHashMap<SQLiteClosable, Object> mPrograms;
private Transaction transaction;
private boolean throwOnInsert;
private Set<Cursor> cursors = new HashSet<Cursor>();
private List<String> querySql = new ArrayList<String>();
@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) {
try {
return insertOrThrow(table, nullColumnHack, values);
} catch (android.database.SQLException e) {
return -1;
}
}
@Implementation
public long insertOrThrow(String table, String nullColumnHack, ContentValues values) throws android.database.SQLException {
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) {
try {
return replaceOrThrow(table, nullColumnHack, values);
} catch (android.database.SQLException e) {
return -1;
}
}
@Implementation
public long replaceOrThrow(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) throws android.database.SQLException {
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;
while (columns.hasNext()) {
insert.setObject(i++, columns.next());
}
insert.executeUpdate();
return fetchGeneratedKey(insert.getGeneratedKeys());
} catch (SQLException e) {
throw new android.database.SQLException(e.getLocalizedMessage());
}
}
@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);
querySql.add(sql);
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);
cursors.add(cursor);
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(DEFAULT_CURSOR_FACTORY, 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);
}
if(cursorFactory == null){
cursorFactory = DEFAULT_CURSOR_FACTORY;
}
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);
cursors.add(cursor);
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);
}
if (transaction == null) {
transaction = new Transaction();
} else {
transaction = new Transaction(transaction);
}
}
@Implementation
public void setTransactionSuccessful() {
if (!isOpen()) {
throw new IllegalStateException("connection is not opened");
} else if (transaction.success) {
throw new IllegalStateException("transaction already successfully");
}
transaction.success = true;
}
@Implementation
public void endTransaction() {
if (transaction.parent != null) {
transaction.parent.descendantsSuccess &= transaction.success;
transaction = transaction.parent;
} else {
try {
if (transaction.success && transaction.descendantsSuccess) {
connection.commit();
} else {
connection.rollback();
}
connection.setAutoCommit(true);
} catch (SQLException e) {
throw new RuntimeException("SQL exception in beginTransaction", e);
}
transaction = null;
}
}
@Implementation
public boolean inTransaction() {
return transaction != null;
}
/**
* Allows tests cases to query the transaction state
* @return
*/
public boolean isTransactionSuccess() {
return transaction != null && transaction.success && transaction.descendantsSuccess;
}
/**
* 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();
}
}
public boolean hasOpenCursors() {
for (Cursor cursor : cursors) {
if (!cursor.isClosed()) {
return true;
}
}
return false;
}
public List<String> getQuerySql() {
return querySql;
}
private static class Transaction {
final Transaction parent;
boolean success;
boolean descendantsSuccess = true;
Transaction(Transaction parent) {
this.parent = parent;
}
Transaction() {
this.parent = null;
}
}
}