SQLiteCursor.java revision 54b6cfa9a9e5b861a9930af873580d6dc20f773c
1/*
2 * Copyright (C) 2006 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.database.sqlite;
18
19import android.database.AbstractWindowedCursor;
20import android.database.CursorWindow;
21import android.database.SQLException;
22import android.text.TextUtils;
23import android.util.Config;
24import android.util.Log;
25
26import java.util.HashMap;
27import java.util.Iterator;
28import java.util.Map;
29
30/**
31 * A Cursor implementation that exposes results from a query on a
32 * {@link SQLiteDatabase}.
33 */
34public class SQLiteCursor extends AbstractWindowedCursor {
35    static final String TAG = "Cursor";
36    static final int NO_COUNT = -1;
37
38    /** The name of the table to edit */
39    private String mEditTable;
40
41    /** The names of the columns in the rows */
42    private String[] mColumns;
43
44    /** The query object for the cursor */
45    private SQLiteQuery mQuery;
46
47    /** The database the cursor was created from */
48    private SQLiteDatabase mDatabase;
49
50    /** The compiled query this cursor came from */
51    private SQLiteCursorDriver mDriver;
52
53    /** The number of rows in the cursor */
54    private int mCount = NO_COUNT;
55
56    /** A mapping of column names to column indices, to speed up lookups */
57    private Map<String, Integer> mColumnNameMap;
58
59    /** Used to find out where a cursor was allocated in case it never got
60     * released. */
61    private StackTraceElement[] mStackTraceElements;
62
63    /**
64     * Execute a query and provide access to its result set through a Cursor
65     * interface. For a query such as: {@code SELECT name, birth, phone FROM
66     * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth,
67     * phone) would be in the projection argument and everything from
68     * {@code FROM} onward would be in the params argument. This constructor
69     * has package scope.
70     *
71     * @param db a reference to a Database object that is already constructed
72     *     and opened
73     * @param editTable the name of the table used for this query
74     * @param query the rest of the query terms
75     *     cursor is finalized
76     */
77    public SQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver,
78            String editTable, SQLiteQuery query) {
79        // The AbstractCursor constructor needs to do some setup.
80        super();
81
82        if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) {
83            mStackTraceElements = new Exception().getStackTrace();
84        }
85
86        mDatabase = db;
87        mDriver = driver;
88        mEditTable = editTable;
89        mColumnNameMap = null;
90        mQuery = query;
91
92        try {
93            db.lock();
94
95            // Setup the list of columns
96            int columnCount = mQuery.columnCountLocked();
97            mColumns = new String[columnCount];
98
99            // Read in all column names
100            for (int i = 0; i < columnCount; i++) {
101                String columnName = mQuery.columnNameLocked(i);
102                mColumns[i] = columnName;
103                if (Config.LOGV) {
104                    Log.v("DatabaseWindow", "mColumns[" + i + "] is "
105                            + mColumns[i]);
106                }
107
108                // Make note of the row ID column index for quick access to it
109                if ("_id".equals(columnName)) {
110                    mRowIdColumnIndex = i;
111                }
112            }
113        } finally {
114            db.unlock();
115        }
116    }
117
118    /**
119     * @return the SQLiteDatabase that this cursor is associated with.
120     */
121    public SQLiteDatabase getDatabase() {
122        return mDatabase;
123    }
124
125    @Override
126    public boolean onMove(int oldPosition, int newPosition) {
127        // Make sure the row at newPosition is present in the window
128        if (mWindow == null || newPosition < mWindow.getStartPosition() ||
129                newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) {
130            fillWindow(newPosition);
131        }
132
133        return true;
134    }
135
136    @Override
137    public int getCount() {
138        if (mCount == NO_COUNT) {
139            fillWindow(0);
140        }
141        return mCount;
142    }
143
144    private void fillWindow (int startPos) {
145        if (mWindow == null) {
146            // If there isn't a window set already it will only be accessed locally
147            mWindow = new CursorWindow(true /* the window is local only */);
148        } else {
149            mWindow.clear();
150        }
151
152        // mWindow must be cleared
153        mCount = mQuery.fillWindow(mWindow, startPos);
154    }
155
156    @Override
157    public int getColumnIndex(String columnName) {
158        // Create mColumnNameMap on demand
159        if (mColumnNameMap == null) {
160            String[] columns = mColumns;
161            int columnCount = columns.length;
162            HashMap<String, Integer> map = new HashMap<String, Integer>(columnCount, 1);
163            for (int i = 0; i < columnCount; i++) {
164                map.put(columns[i], i);
165            }
166            mColumnNameMap = map;
167        }
168
169        // Hack according to bug 903852
170        final int periodIndex = columnName.lastIndexOf('.');
171        if (periodIndex != -1) {
172            Exception e = new Exception();
173            Log.e(TAG, "requesting column name with table name -- " + columnName, e);
174            columnName = columnName.substring(periodIndex + 1);
175        }
176
177        Integer i = mColumnNameMap.get(columnName);
178        if (i != null) {
179            return i.intValue();
180        } else {
181            return -1;
182        }
183    }
184
185    /**
186     * @hide
187     * @deprecated
188     */
189    @Override
190    public boolean deleteRow() {
191        checkPosition();
192
193        // Only allow deletes if there is an ID column, and the ID has been read from it
194        if (mRowIdColumnIndex == -1 || mCurrentRowID == null) {
195            Log.e(TAG,
196                    "Could not delete row because either the row ID column is not available or it" +
197                    "has not been read.");
198            return false;
199        }
200
201        boolean success;
202
203        /*
204         * Ensure we don't change the state of the database when another
205         * thread is holding the database lock. requery() and moveTo() are also
206         * synchronized here to make sure they get the state of the database
207         * immediately following the DELETE.
208         */
209        mDatabase.lock();
210        try {
211            try {
212                mDatabase.delete(mEditTable, mColumns[mRowIdColumnIndex] + "=?",
213                        new String[] {mCurrentRowID.toString()});
214                success = true;
215            } catch (SQLException e) {
216                success = false;
217            }
218
219            int pos = mPos;
220            requery();
221
222            /*
223             * Ensure proper cursor state. Note that mCurrentRowID changes
224             * in this call.
225             */
226            moveToPosition(pos);
227        } finally {
228            mDatabase.unlock();
229        }
230
231        if (success) {
232            onChange(true);
233            return true;
234        } else {
235            return false;
236        }
237    }
238
239    @Override
240    public String[] getColumnNames() {
241        return mColumns;
242    }
243
244    /**
245     * @hide
246     * @deprecated
247     */
248    @Override
249    public boolean supportsUpdates() {
250        return super.supportsUpdates() && !TextUtils.isEmpty(mEditTable);
251    }
252
253    /**
254     * @hide
255     * @deprecated
256     */
257    @Override
258    public boolean commitUpdates(Map<? extends Long,
259            ? extends Map<String, Object>> additionalValues) {
260        if (!supportsUpdates()) {
261            Log.e(TAG, "commitUpdates not supported on this cursor, did you "
262                    + "include the _id column?");
263            return false;
264        }
265
266        /*
267         * Prevent other threads from changing the updated rows while they're
268         * being processed here.
269         */
270        synchronized (mUpdatedRows) {
271            if (additionalValues != null) {
272                mUpdatedRows.putAll(additionalValues);
273            }
274
275            if (mUpdatedRows.size() == 0) {
276                return true;
277            }
278
279            /*
280             * Prevent other threads from changing the database state while
281             * we process the updated rows, and prevents us from changing the
282             * database behind the back of another thread.
283             */
284            mDatabase.beginTransaction();
285            try {
286                StringBuilder sql = new StringBuilder(128);
287
288                // For each row that has been updated
289                for (Map.Entry<Long, Map<String, Object>> rowEntry :
290                        mUpdatedRows.entrySet()) {
291                    Map<String, Object> values = rowEntry.getValue();
292                    Long rowIdObj = rowEntry.getKey();
293
294                    if (rowIdObj == null || values == null) {
295                        throw new IllegalStateException("null rowId or values found! rowId = "
296                                + rowIdObj + ", values = " + values);
297                    }
298
299                    if (values.size() == 0) {
300                        continue;
301                    }
302
303                    long rowId = rowIdObj.longValue();
304
305                    Iterator<Map.Entry<String, Object>> valuesIter =
306                            values.entrySet().iterator();
307
308                    sql.setLength(0);
309                    sql.append("UPDATE " + mEditTable + " SET ");
310
311                    // For each column value that has been updated
312                    Object[] bindings = new Object[values.size()];
313                    int i = 0;
314                    while (valuesIter.hasNext()) {
315                        Map.Entry<String, Object> entry = valuesIter.next();
316                        sql.append(entry.getKey());
317                        sql.append("=?");
318                        bindings[i] = entry.getValue();
319                        if (valuesIter.hasNext()) {
320                            sql.append(", ");
321                        }
322                        i++;
323                    }
324
325                    sql.append(" WHERE " + mColumns[mRowIdColumnIndex]
326                            + '=' + rowId);
327                    sql.append(';');
328                    mDatabase.execSQL(sql.toString(), bindings);
329                    mDatabase.rowUpdated(mEditTable, rowId);
330                }
331                mDatabase.setTransactionSuccessful();
332            } finally {
333                mDatabase.endTransaction();
334            }
335
336            mUpdatedRows.clear();
337        }
338
339        // Let any change observers know about the update
340        onChange(true);
341
342        return true;
343    }
344
345    private void deactivateCommon() {
346        if (Config.LOGV) Log.v(TAG, "<<< Releasing cursor " + this);
347        if (mWindow != null) {
348            mWindow.close();
349            mWindow = null;
350        }
351        if (Config.LOGV) Log.v("DatabaseWindow", "closing window in release()");
352    }
353
354    @Override
355    public void deactivate() {
356        super.deactivate();
357        deactivateCommon();
358        mDriver.cursorDeactivated();
359    }
360
361    @Override
362    public void close() {
363        super.close();
364        deactivateCommon();
365        mQuery.close();
366        mDriver.cursorClosed();
367    }
368
369    @Override
370    public boolean requery() {
371        long timeStart = 0;
372        if (Config.LOGV) {
373            timeStart = System.currentTimeMillis();
374        }
375        /*
376         * Synchronize on the database lock to ensure that mCount matches the
377         * results of mQuery.requery().
378         */
379        mDatabase.lock();
380        try {
381            if (mWindow != null) {
382                mWindow.clear();
383            }
384            mPos = -1;
385            // This one will recreate the temp table, and get its count
386            mDriver.cursorRequeried(this);
387            mCount = NO_COUNT;
388            // Requery the program that runs over the temp table
389            mQuery.requery();
390        } finally {
391            mDatabase.unlock();
392        }
393
394        if (Config.LOGV) {
395            Log.v("DatabaseWindow", "closing window in requery()");
396            Log.v(TAG, "--- Requery()ed cursor " + this + ": " + mQuery);
397        }
398
399        boolean result = super.requery();
400        if (Config.LOGV) {
401            long timeEnd = System.currentTimeMillis();
402            Log.v(TAG, "requery (" + (timeEnd - timeStart) + " ms): " + mDriver.toString());
403        }
404        return result;
405    }
406
407    @Override
408    public void setWindow(CursorWindow window) {
409        if (mWindow != null) {
410            mWindow.close();
411            mCount = NO_COUNT;
412        }
413        mWindow = window;
414    }
415
416    /**
417     * Changes the selection arguments. The new values take effect after a call to requery().
418     */
419    public void setSelectionArguments(String[] selectionArgs) {
420        mDriver.setBindArguments(selectionArgs);
421    }
422
423    /**
424     * Release the native resources, if they haven't been released yet.
425     */
426    @Override
427    protected void finalize() {
428        try {
429            if (mWindow != null) {
430                close();
431                String message = "Finalizing cursor " + this + " on " + mEditTable
432                        + " that has not been deactivated or closed";
433                if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) {
434                    Log.d(TAG, message + "\nThis cursor was created in:");
435                    for (StackTraceElement ste : mStackTraceElements) {
436                        Log.d(TAG, "      " + ste);
437                    }
438                }
439                SQLiteDebug.notifyActiveCursorFinalized();
440                throw new IllegalStateException(message);
441            } else {
442                if (Config.LOGV) {
443                    Log.v(TAG, "Finalizing cursor " + this + " on " + mEditTable);
444                }
445            }
446        } finally {
447            super.finalize();
448        }
449    }
450}
451