Home | History | Annotate | Download | only in shadows
      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
     47     public void setLockingEnabled(boolean lockingEnabled) {
     48         mLockingEnabled = lockingEnabled;
     49     }
     50 
     51     public void lock() {
     52         if (!mLockingEnabled) return;
     53         mLock.lock();
     54     }
     55 
     56     public void unlock() {
     57         if (!mLockingEnabled) return;
     58         mLock.unlock();
     59     }
     60 
     61     public void setThrowOnInsert(boolean throwOnInsert) {
     62         this.throwOnInsert = throwOnInsert;
     63     }
     64 
     65     @Implementation
     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
     72     public long insert(String table, String nullColumnHack, ContentValues values) {
     73         return insertWithOnConflict(table, nullColumnHack, values, SQLiteDatabase.CONFLICT_NONE);
     74     }
     75 
     76     @Implementation
     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
     84     public long replace(String table, String nullColumnHack, ContentValues values) {
     85         return insertWithOnConflict(table, nullColumnHack, values, SQLiteDatabase.CONFLICT_REPLACE);
     86     }
     87 
     88     @Implementation
     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
    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
    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
    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
    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
    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
    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
    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
    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