• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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