1package com.xtremelabs.robolectric.shadows;
2
3import android.content.ContentValues;
4import android.database.Cursor;
5import android.database.DatabaseUtils;
6import android.database.sqlite.*;
7import com.xtremelabs.robolectric.Robolectric;
8import com.xtremelabs.robolectric.internal.Implementation;
9import com.xtremelabs.robolectric.internal.Implements;
10import com.xtremelabs.robolectric.internal.RealObject;
11import com.xtremelabs.robolectric.util.DatabaseConfig;
12import com.xtremelabs.robolectric.util.SQLite.SQLStringAndBindings;
13
14import java.sql.Connection;
15import java.sql.PreparedStatement;
16import java.sql.ResultSet;
17import java.sql.SQLException;
18import java.sql.Statement;
19import java.util.Iterator;
20import java.util.WeakHashMap;
21import java.util.concurrent.locks.ReentrantLock;
22
23import static com.xtremelabs.robolectric.Robolectric.newInstanceOf;
24import static com.xtremelabs.robolectric.Robolectric.shadowOf;
25import static com.xtremelabs.robolectric.util.SQLite.buildDeleteString;
26import static com.xtremelabs.robolectric.util.SQLite.buildInsertString;
27import static com.xtremelabs.robolectric.util.SQLite.buildUpdateString;
28import 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)
36public 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