1 package com.xtremelabs.robolectric.shadows; 2 3 import android.content.ContentValues; 4 import android.database.Cursor; 5 import android.database.DatabaseUtils; 6 import android.database.sqlite.*; 7 import com.xtremelabs.robolectric.Robolectric; 8 import com.xtremelabs.robolectric.internal.Implementation; 9 import com.xtremelabs.robolectric.internal.Implements; 10 import com.xtremelabs.robolectric.internal.RealObject; 11 import com.xtremelabs.robolectric.util.DatabaseConfig; 12 import com.xtremelabs.robolectric.util.SQLite.SQLStringAndBindings; 13 14 import java.sql.Connection; 15 import java.sql.PreparedStatement; 16 import java.sql.ResultSet; 17 import java.sql.SQLException; 18 import java.sql.Statement; 19 import java.util.Iterator; 20 import java.util.WeakHashMap; 21 import java.util.concurrent.locks.ReentrantLock; 22 23 import static com.xtremelabs.robolectric.Robolectric.newInstanceOf; 24 import static com.xtremelabs.robolectric.Robolectric.shadowOf; 25 import static com.xtremelabs.robolectric.util.SQLite.buildDeleteString; 26 import static com.xtremelabs.robolectric.util.SQLite.buildInsertString; 27 import static com.xtremelabs.robolectric.util.SQLite.buildUpdateString; 28 import static com.xtremelabs.robolectric.util.SQLite.buildWhereClause; 29 30 /** 31 * Shadow for {@code SQLiteDatabase} that simulates the movement of a {@code Cursor} through database tables. 32 * Implemented as a wrapper around an embedded SQL database, accessed via JDBC. The JDBC connection is 33 * made available to test cases for use in fixture setup and assertions. 34 */ 35 @Implements(SQLiteDatabase.class) 36 public class ShadowSQLiteDatabase { 37 @RealObject SQLiteDatabase realSQLiteDatabase; 38 private static Connection connection; 39 private final ReentrantLock mLock = new ReentrantLock(true); 40 private boolean mLockingEnabled = true; 41 private WeakHashMap<SQLiteClosable, Object> mPrograms; 42 private boolean inTransaction = false; 43 private boolean transactionSuccess = false; 44 private boolean throwOnInsert; 45 46 @Implementation setLockingEnabled(boolean lockingEnabled)47 public void setLockingEnabled(boolean lockingEnabled) { 48 mLockingEnabled = lockingEnabled; 49 } 50 lock()51 public void lock() { 52 if (!mLockingEnabled) return; 53 mLock.lock(); 54 } 55 unlock()56 public void unlock() { 57 if (!mLockingEnabled) return; 58 mLock.unlock(); 59 } 60 setThrowOnInsert(boolean throwOnInsert)61 public void setThrowOnInsert(boolean throwOnInsert) { 62 this.throwOnInsert = throwOnInsert; 63 } 64 65 @Implementation openDatabase(String path, SQLiteDatabase.CursorFactory factory, int flags)66 public static SQLiteDatabase openDatabase(String path, SQLiteDatabase.CursorFactory factory, int flags) { 67 connection = DatabaseConfig.getMemoryConnection(); 68 return newInstanceOf(SQLiteDatabase.class); 69 } 70 71 @Implementation insert(String table, String nullColumnHack, ContentValues values)72 public long insert(String table, String nullColumnHack, ContentValues values) { 73 return insertWithOnConflict(table, nullColumnHack, values, SQLiteDatabase.CONFLICT_NONE); 74 } 75 76 @Implementation insertOrThrow(String table, String nullColumnHack, ContentValues values)77 public long insertOrThrow(String table, String nullColumnHack, ContentValues values) { 78 if (throwOnInsert) 79 throw new android.database.SQLException(); 80 return insertWithOnConflict(table, nullColumnHack, values, SQLiteDatabase.CONFLICT_NONE); 81 } 82 83 @Implementation replace(String table, String nullColumnHack, ContentValues values)84 public long replace(String table, String nullColumnHack, ContentValues values) { 85 return insertWithOnConflict(table, nullColumnHack, values, SQLiteDatabase.CONFLICT_REPLACE); 86 } 87 88 @Implementation insertWithOnConflict(String table, String nullColumnHack, ContentValues initialValues, int conflictAlgorithm)89 public long insertWithOnConflict(String table, String nullColumnHack, 90 ContentValues initialValues, int conflictAlgorithm) { 91 92 try { 93 SQLStringAndBindings sqlInsertString = buildInsertString(table, initialValues, conflictAlgorithm); 94 PreparedStatement insert = connection.prepareStatement(sqlInsertString.sql, Statement.RETURN_GENERATED_KEYS); 95 Iterator<Object> columns = sqlInsertString.columnValues.iterator(); 96 int i = 1; 97 long result = -1; 98 while (columns.hasNext()) { 99 insert.setObject(i++, columns.next()); 100 } 101 insert.executeUpdate(); 102 ResultSet resultSet = insert.getGeneratedKeys(); 103 if (resultSet.next()) { 104 result = resultSet.getLong(1); 105 } 106 resultSet.close(); 107 return result; 108 } catch (SQLException e) { 109 return -1; // this is how SQLite behaves, unlike H2 which throws exceptions 110 } 111 } 112 113 @Implementation query(boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit)114 public Cursor query(boolean distinct, String table, String[] columns, 115 String selection, String[] selectionArgs, String groupBy, 116 String having, String orderBy, String limit) { 117 118 String where = selection; 119 if (selection != null && selectionArgs != null) { 120 where = buildWhereClause(selection, selectionArgs); 121 } 122 123 String sql = SQLiteQueryBuilder.buildQueryString(distinct, table, 124 columns, where, groupBy, having, orderBy, limit); 125 126 ResultSet resultSet; 127 try { 128 Statement statement = connection.createStatement(DatabaseConfig.getResultSetType(), ResultSet.CONCUR_READ_ONLY); 129 resultSet = statement.executeQuery(sql); 130 } catch (SQLException e) { 131 throw new RuntimeException("SQL exception in query", e); 132 } 133 134 SQLiteCursor cursor = new SQLiteCursor(null, null, null, null); 135 shadowOf(cursor).setResultSet(resultSet,sql); 136 return cursor; 137 } 138 139 @Implementation query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy)140 public Cursor query(String table, String[] columns, String selection, 141 String[] selectionArgs, String groupBy, String having, 142 String orderBy) { 143 return query(false, table, columns, selection, selectionArgs, groupBy, having, orderBy, null); 144 } 145 146 @Implementation query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit)147 public Cursor query(String table, String[] columns, String selection, 148 String[] selectionArgs, String groupBy, String having, 149 String orderBy, String limit) { 150 return query(false, table, columns, selection, selectionArgs, groupBy, having, orderBy, limit); 151 } 152 153 @Implementation update(String table, ContentValues values, String whereClause, String[] whereArgs)154 public int update(String table, ContentValues values, String whereClause, String[] whereArgs) { 155 SQLStringAndBindings sqlUpdateString = buildUpdateString(table, values, whereClause, whereArgs); 156 157 try { 158 PreparedStatement statement = connection.prepareStatement(sqlUpdateString.sql); 159 Iterator<Object> columns = sqlUpdateString.columnValues.iterator(); 160 int i = 1; 161 while (columns.hasNext()) { 162 statement.setObject(i++, columns.next()); 163 } 164 165 return statement.executeUpdate(); 166 } catch (SQLException e) { 167 throw new RuntimeException("SQL exception in update", e); 168 } 169 } 170 171 @Implementation delete(String table, String whereClause, String[] whereArgs)172 public int delete(String table, String whereClause, String[] whereArgs) { 173 String sql = buildDeleteString(table, whereClause, whereArgs); 174 175 try { 176 return connection.prepareStatement(sql).executeUpdate(); 177 } catch (SQLException e) { 178 throw new RuntimeException("SQL exception in delete", e); 179 } 180 } 181 182 @Implementation execSQL(String sql)183 public void execSQL(String sql) throws android.database.SQLException { 184 if (!isOpen()) { 185 throw new IllegalStateException("database not open"); 186 } 187 188 try { 189 String scrubbedSql= DatabaseConfig.getScrubSQL(sql); 190 connection.createStatement().execute(scrubbedSql); 191 } catch (java.sql.SQLException e) { 192 android.database.SQLException ase = new android.database.SQLException(); 193 ase.initCause(e); 194 throw ase; 195 } 196 } 197 198 @Implementation execSQL(String sql, Object[] bindArgs)199 public void execSQL(String sql, Object[] bindArgs) throws SQLException { 200 if (bindArgs == null) { 201 throw new IllegalArgumentException("Empty bindArgs"); 202 } 203 String scrubbedSql= DatabaseConfig.getScrubSQL(sql); 204 205 206 SQLiteStatement statement = null; 207 try { 208 statement =compileStatement(scrubbedSql); 209 if (bindArgs != null) { 210 int numArgs = bindArgs.length; 211 for (int i = 0; i < numArgs; i++) { 212 DatabaseUtils.bindObjectToProgram(statement, i + 1, bindArgs[i]); 213 } 214 } 215 statement.execute(); 216 } catch (SQLiteDatabaseCorruptException e) { 217 throw e; 218 } finally { 219 if (statement != null) { 220 statement.close(); 221 } 222 } 223 } 224 225 226 @Implementation rawQuery(String sql, String[] selectionArgs)227 public Cursor rawQuery (String sql, String[] selectionArgs) { 228 return rawQueryWithFactory( new SQLiteDatabase.CursorFactory() { 229 @Override 230 public Cursor newCursor(SQLiteDatabase db, 231 SQLiteCursorDriver masterQuery, String editTable, SQLiteQuery query) { 232 return new SQLiteCursor(db, masterQuery, editTable, query); 233 } 234 235 }, sql, selectionArgs, null ); 236 } 237 238 @Implementation 239 public Cursor rawQueryWithFactory (SQLiteDatabase.CursorFactory cursorFactory, String sql, String[] selectionArgs, String editTable) { 240 String sqlBody = sql; 241 if (sql != null) { 242 sqlBody = buildWhereClause(sql, selectionArgs); 243 } 244 245 ResultSet resultSet; 246 try { 247 SQLiteStatement stmt = compileStatement(sql); 248 249 int numArgs = selectionArgs == null ? 0 250 : selectionArgs.length; 251 for (int i = 0; i < numArgs; i++) { 252 stmt.bindString(i + 1, selectionArgs[i]); 253 } 254 255 resultSet = Robolectric.shadowOf(stmt).getStatement().executeQuery(); 256 } catch (SQLException e) { 257 throw new RuntimeException("SQL exception in query", e); 258 } 259 //TODO: assert rawquery with args returns actual values 260 261 SQLiteCursor cursor = (SQLiteCursor) cursorFactory.newCursor(null, null, null, null); 262 shadowOf(cursor).setResultSet(resultSet, sqlBody); 263 return cursor; 264 } 265 266 @Implementation 267 public boolean isOpen() { 268 return (connection != null); 269 } 270 271 @Implementation 272 public void close() { 273 if (!isOpen()) { 274 return; 275 } 276 try { 277 connection.close(); 278 connection = null; 279 } catch (SQLException e) { 280 throw new RuntimeException("SQL exception in close", e); 281 } 282 } 283 284 @Implementation 285 public void beginTransaction() { 286 try { 287 connection.setAutoCommit(false); 288 } catch (SQLException e) { 289 throw new RuntimeException("SQL exception in beginTransaction", e); 290 } finally { 291 inTransaction = true; 292 } 293 } 294 295 @Implementation 296 public void setTransactionSuccessful() { 297 if (!isOpen()) { 298 throw new IllegalStateException("connection is not opened"); 299 } else if (transactionSuccess) { 300 throw new IllegalStateException("transaction already successfully"); 301 } 302 transactionSuccess = true; 303 } 304 305 @Implementation 306 public void endTransaction() { 307 try { 308 if (transactionSuccess) { 309 transactionSuccess = false; 310 connection.commit(); 311 } else { 312 connection.rollback(); 313 } 314 connection.setAutoCommit(true); 315 } catch (SQLException e) { 316 throw new RuntimeException("SQL exception in beginTransaction", e); 317 } finally { 318 inTransaction = false; 319 } 320 } 321 322 @Implementation 323 public boolean inTransaction() { 324 return inTransaction; 325 } 326 327 /** 328 * Allows tests cases to query the transaction state 329 * @return 330 */ 331 public boolean isTransactionSuccess() { 332 return transactionSuccess; 333 } 334 335 /** 336 * Allows test cases access to the underlying JDBC connection, for use in 337 * setup or assertions. 338 * 339 * @return the connection 340 */ 341 public Connection getConnection() { 342 return connection; 343 } 344 345 @Implementation 346 public SQLiteStatement compileStatement(String sql) throws SQLException { 347 lock(); 348 String scrubbedSql= DatabaseConfig.getScrubSQL(sql); 349 try { 350 SQLiteStatement stmt = Robolectric.newInstanceOf(SQLiteStatement.class); 351 Robolectric.shadowOf(stmt).init(realSQLiteDatabase, scrubbedSql); 352 return stmt; 353 } catch (Exception e){ 354 throw new RuntimeException(e); 355 } finally { 356 unlock(); 357 } 358 } 359 360 /** 361 * @param closable 362 */ 363 void addSQLiteClosable(SQLiteClosable closable) { 364 lock(); 365 try { 366 mPrograms.put(closable, null); 367 } finally { 368 unlock(); 369 } 370 } 371 372 void removeSQLiteClosable(SQLiteClosable closable) { 373 lock(); 374 try { 375 mPrograms.remove(closable); 376 } finally { 377 unlock(); 378 } 379 } 380 381 } 382