ContentProviderOperation.java revision 8943737692169f564cd34a9c8d471f3a5d438712
1/*
2 * Copyright (C) 2009 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.content;
18
19import android.net.Uri;
20import android.database.Cursor;
21import android.os.Parcelable;
22import android.os.Parcel;
23
24import java.util.Map;
25import java.util.HashMap;
26
27import dalvik.system.VMStack;
28
29public class ContentProviderOperation implements Parcelable {
30    private final static int TYPE_INSERT = 1;
31    private final static int TYPE_UPDATE = 2;
32    private final static int TYPE_DELETE = 3;
33    private final static int TYPE_COUNT = 4;
34
35    private final int mType;
36    private final Uri mUri;
37    private final String mSelection;
38    private final String[] mSelectionArgs;
39    private final ContentValues mValues;
40    private final Entity mEntity;
41    private final Integer mExpectedCount;
42    private final ContentValues mValuesBackReferences;
43    private final Map<Integer, Integer> mSelectionArgsBackReferences;
44
45    private static final String[] COUNT_COLUMNS = new String[]{"count(*)"};
46
47    /**
48     * Creates a {@link ContentProviderOperation} by copying the contents of a
49     * {@link Builder}.
50     */
51    private ContentProviderOperation(Builder builder) {
52        mType = builder.mType;
53        mUri = builder.mUri;
54        mValues = builder.mValues;
55        mEntity = builder.mEntity;
56        mSelection = builder.mSelection;
57        mSelectionArgs = builder.mSelectionArgs;
58        mExpectedCount = builder.mExpectedCount;
59        mSelectionArgsBackReferences = builder.mSelectionArgsBackReferences;
60        mValuesBackReferences = builder.mValuesBackReferences;
61    }
62
63    private ContentProviderOperation(Parcel source, ClassLoader classLoader) {
64        mType = source.readInt();
65        mUri = Uri.CREATOR.createFromParcel(source);
66        mValues = source.readInt() != 0 ? ContentValues.CREATOR.createFromParcel(source) : null;
67        mEntity = (Entity) source.readParcelable(classLoader);
68        mSelection = source.readInt() != 0 ? source.readString() : null;
69        mSelectionArgs = source.readInt() != 0 ? source.readStringArray() : null;
70        mExpectedCount = source.readInt() != 0 ? source.readInt() : null;
71        mValuesBackReferences = source.readInt() != 0
72
73                ? ContentValues.CREATOR.createFromParcel(source)
74                : null;
75        mSelectionArgsBackReferences = source.readInt() != 0
76                ? new HashMap<Integer, Integer>()
77                : null;
78        if (mSelectionArgsBackReferences != null) {
79            final int count = source.readInt();
80            for (int i = 0; i < count; i++) {
81                mSelectionArgsBackReferences.put(source.readInt(), source.readInt());
82            }
83        }
84    }
85
86    public void writeToParcel(Parcel dest, int flags) {
87        dest.writeInt(mType);
88        Uri.writeToParcel(dest, mUri);
89        if (mValues != null) {
90            dest.writeInt(1);
91            mValues.writeToParcel(dest, 0);
92        } else {
93            dest.writeInt(0);
94        }
95        dest.writeParcelable(mEntity, 0);
96        if (mSelection != null) {
97            dest.writeInt(1);
98            dest.writeString(mSelection);
99        } else {
100            dest.writeInt(0);
101        }
102        if (mSelectionArgs != null) {
103            dest.writeInt(1);
104            dest.writeStringArray(mSelectionArgs);
105        } else {
106            dest.writeInt(0);
107        }
108        if (mExpectedCount != null) {
109            dest.writeInt(1);
110            dest.writeInt(mExpectedCount);
111        } else {
112            dest.writeInt(0);
113        }
114        if (mValuesBackReferences != null) {
115            dest.writeInt(1);
116            mValuesBackReferences.writeToParcel(dest, 0);
117        } else {
118            dest.writeInt(0);
119        }
120        if (mSelectionArgsBackReferences != null) {
121            dest.writeInt(1);
122            dest.writeInt(mSelectionArgsBackReferences.size());
123            for (Map.Entry<Integer, Integer> entry : mSelectionArgsBackReferences.entrySet()) {
124                dest.writeInt(entry.getKey());
125                dest.writeInt(entry.getValue());
126            }
127        } else {
128            dest.writeInt(0);
129        }
130    }
131
132    /**
133     * Create a {@link Builder} suitable for building an insert {@link ContentProviderOperation}.
134     * @param uri The {@link Uri} that is the target of the insert.
135     * @return a {@link Builder}
136     */
137    public static Builder newInsert(Uri uri) {
138        return new Builder(TYPE_INSERT, uri);
139    }
140
141    /**
142     * Create a {@link Builder} suitable for building an update {@link ContentProviderOperation}.
143     * @param uri The {@link Uri} that is the target of the update.
144     * @return a {@link Builder}
145     */
146    public static Builder newUpdate(Uri uri) {
147        return new Builder(TYPE_UPDATE, uri);
148    }
149
150    /**
151     * Create a {@link Builder} suitable for building a delete {@link ContentProviderOperation}.
152     * @param uri The {@link Uri} that is the target of the delete.
153     * @return a {@link Builder}
154     */
155    public static Builder newDelete(Uri uri) {
156        return new Builder(TYPE_DELETE, uri);
157    }
158
159    /**
160     * Create a {@link Builder} suitable for building a count query. When used in conjunction
161     * with {@link Builder#withExpectedCount(int)} this is useful for checking that the
162     * uri/selection has the expected number of rows.
163     * {@link ContentProviderOperation}.
164     * @param uri The {@link Uri} to query.
165     * @return a {@link Builder}
166     */
167    public static Builder newCountQuery(Uri uri) {
168        return new Builder(TYPE_COUNT, uri);
169    }
170
171    public Uri getUri() {
172        return mUri;
173    }
174
175    public boolean isWriteOperation() {
176        return mType == TYPE_DELETE || mType == TYPE_INSERT || mType == TYPE_UPDATE;
177    }
178
179    public boolean isReadOperation() {
180        return mType == TYPE_COUNT;
181    }
182
183    /**
184     * Applies this operation using the given provider. The backRefs array is used to resolve any
185     * back references that were requested using
186     * {@link Builder#withValueBackReferences(ContentValues)} and
187     * {@link Builder#withSelectionBackReferences(java.util.Map)}.
188     * @param provider the {@link ContentProvider} on which this batch is applied
189     * @param backRefs a {@link ContentProviderResult} array that will be consulted
190     * to resolve any requested back references.
191     * @param numBackRefs the number of valid results on the backRefs array.
192     * @return a {@link ContentProviderResult} that contains either the {@link Uri} of the inserted
193     * row if this was an insert otherwise the number of rows affected.
194     * @throws OperationApplicationException thrown if either the insert fails or
195     * if the number of rows affected didn't match the expected count
196     */
197    public ContentProviderResult apply(ContentProvider provider, ContentProviderResult[] backRefs,
198            int numBackRefs) throws OperationApplicationException {
199        ContentValues values = resolveValueBackReferences(backRefs, numBackRefs);
200        String[] selectionArgs =
201                resolveSelectionArgsBackReferences(backRefs, numBackRefs);
202
203        if (mType == TYPE_INSERT) {
204            Uri newUri;
205            if (mEntity != null) {
206                newUri = provider.insertEntity(mUri, mEntity);
207            } else {
208                newUri = provider.insert(mUri, values);
209            }
210            if (newUri == null) {
211                throw new OperationApplicationException("insert failed");
212            }
213            return new ContentProviderResult(newUri);
214        }
215
216        int numRows;
217        if (mType == TYPE_DELETE) {
218            numRows = provider.delete(mUri, mSelection, selectionArgs);
219        } else if (mType == TYPE_UPDATE) {
220            if (mEntity != null) {
221                numRows = provider.updateEntity(mUri, mEntity);
222            } else {
223                numRows = provider.update(mUri, values, mSelection, selectionArgs);
224            }
225        } else if (mType == TYPE_COUNT) {
226            Cursor cursor = provider.query(mUri, COUNT_COLUMNS, mSelection, selectionArgs, null);
227            try {
228                if (!cursor.moveToNext()) {
229                    throw new RuntimeException("since we are doing a count query we should always "
230                            + "be able to move to the first row");
231                }
232                if (cursor.getCount() != 1) {
233                    throw new RuntimeException("since we are doing a count query there should "
234                            + "always be exacly row, found " + cursor.getCount());
235                }
236                numRows = cursor.getInt(0);
237            } finally {
238                cursor.close();
239            }
240        } else {
241            throw new IllegalStateException("bad type, " + mType);
242        }
243
244        if (mExpectedCount != null && mExpectedCount != numRows) {
245            throw new OperationApplicationException("wrong number of rows: " + numRows);
246        }
247
248        return new ContentProviderResult(numRows);
249    }
250
251    /**
252     * The ContentValues back references are represented as a ContentValues object where the
253     * key refers to a column and the value is an index of the back reference whose
254     * valued should be associated with the column.
255     * @param backRefs an array of previous results
256     * @param numBackRefs the number of valid previous results in backRefs
257     * @return the ContentValues that should be used in this operation application after
258     * expansion of back references. This can be called if either mValues or mValuesBackReferences
259     * is null
260     * @VisibleForTesting this is intended to be a private method but it is exposed for
261     * unit testing purposes
262     */
263    public ContentValues resolveValueBackReferences(
264            ContentProviderResult[] backRefs, int numBackRefs) {
265        if (mValuesBackReferences == null) {
266            return mValues;
267        }
268        final ContentValues values;
269        if (mValues == null) {
270            values = new ContentValues();
271        } else {
272            values = new ContentValues(mValues);
273        }
274        for (Map.Entry<String, Object> entry : mValuesBackReferences.valueSet()) {
275            String key = entry.getKey();
276            Integer backRefIndex = mValuesBackReferences.getAsInteger(key);
277            if (backRefIndex == null) {
278                throw new IllegalArgumentException("values backref " + key + " is not an integer");
279            }
280            values.put(key, backRefToValue(backRefs, numBackRefs, backRefIndex));
281        }
282        return values;
283    }
284
285    /**
286     * The Selection Arguments back references are represented as a Map of Integer->Integer where
287     * the key is an index into the selection argument array (see {@link Builder#withSelection})
288     * and the value is the index of the previous result that should be used for that selection
289     * argument array slot.
290     * @param backRefs an array of previous results
291     * @param numBackRefs the number of valid previous results in backRefs
292     * @return the ContentValues that should be used in this operation application after
293     * expansion of back references. This can be called if either mValues or mValuesBackReferences
294     * is null
295     * @VisibleForTesting this is intended to be a private method but it is exposed for
296     * unit testing purposes
297     */
298    public String[] resolveSelectionArgsBackReferences(
299            ContentProviderResult[] backRefs, int numBackRefs) {
300        if (mSelectionArgsBackReferences == null) {
301            return mSelectionArgs;
302        }
303        String[] newArgs = new String[mSelectionArgs.length];
304        System.arraycopy(mSelectionArgs, 0, newArgs, 0, mSelectionArgs.length);
305        for (Map.Entry<Integer, Integer> selectionArgBackRef
306                : mSelectionArgsBackReferences.entrySet()) {
307            final Integer selectionArgIndex = selectionArgBackRef.getKey();
308            final int backRefIndex = selectionArgBackRef.getValue();
309            newArgs[selectionArgIndex] = backRefToValue(backRefs, numBackRefs, backRefIndex);
310        }
311        return newArgs;
312    }
313
314    /**
315     * Return the string representation of the requested back reference.
316     * @param backRefs an array of results
317     * @param numBackRefs the number of items in the backRefs array that are valid
318     * @param backRefIndex which backRef to be used
319     * @throws ArrayIndexOutOfBoundsException thrown if the backRefIndex is larger than
320     * the numBackRefs
321     * @return the string representation of the requested back reference.
322     */
323    private static String backRefToValue(ContentProviderResult[] backRefs, int numBackRefs,
324            Integer backRefIndex) {
325        if (backRefIndex > numBackRefs) {
326            throw new ArrayIndexOutOfBoundsException("asked for back ref " + backRefIndex
327                    + " but there are only " + numBackRefs + " back refs");
328        }
329        ContentProviderResult backRef = backRefs[backRefIndex];
330        String backRefValue;
331        if (backRef.uri != null) {
332            backRefValue = backRef.uri.getLastPathSegment();
333        } else {
334            backRefValue = String.valueOf(backRef.count);
335        }
336        return backRefValue;
337    }
338
339    public int describeContents() {
340        return 0;
341    }
342
343    public static final Creator<ContentProviderOperation> CREATOR =
344            new Creator<ContentProviderOperation>() {
345        public ContentProviderOperation createFromParcel(Parcel source) {
346            return new ContentProviderOperation(source, VMStack.getCallingClassLoader2());
347        }
348
349        public ContentProviderOperation[] newArray(int size) {
350            return new ContentProviderOperation[size];
351        }
352    };
353
354
355    /**
356     * Used to add parameters to a {@link ContentProviderOperation}. The {@link Builder} is
357     * first created by calling {@link ContentProviderOperation#newInsert(android.net.Uri)},
358     * {@link ContentProviderOperation#newUpdate(android.net.Uri)},
359     * {@link ContentProviderOperation#newDelete(android.net.Uri)} or
360     * {@link ContentProviderOperation#newCountQuery(android.net.Uri)}. The withXXX methods
361     * can then be used to add parameters to the builder. See the specific methods to find for
362     * which {@link Builder} type each is allowed. Call {@link #build} to create the
363     * {@link ContentProviderOperation} once all the parameters have been supplied.
364     */
365    public static class Builder {
366        private final int mType;
367        private final Uri mUri;
368        private String mSelection;
369        private String[] mSelectionArgs;
370        private ContentValues mValues;
371        private Entity mEntity;
372        private Integer mExpectedCount;
373        private ContentValues mValuesBackReferences;
374        private Map<Integer, Integer> mSelectionArgsBackReferences;
375
376        /** Create a {@link Builder} of a given type. The uri must not be null. */
377        private Builder(int type, Uri uri) {
378            if (uri == null) {
379                throw new IllegalArgumentException("uri must not be null");
380            }
381            mType = type;
382            mUri = uri;
383        }
384
385        /** Create a ContentProviderOperation from this {@link Builder}. */
386        public ContentProviderOperation build() {
387            if (mValues != null && mEntity != null) {
388                throw new IllegalArgumentException("you are not allowed to specify both an entity "
389                        + "and a values");
390            }
391            if (mEntity != null && mValuesBackReferences != null) {
392                throw new IllegalArgumentException("you are not allowed to specify both an entity "
393                        + "and a values backreference");
394            }
395            return new ContentProviderOperation(this);
396        }
397
398        /**
399         * Add a {@link ContentValues} of back references. The key is the name of the column
400         * and the value is an integer that is the index of the previous result whose
401         * value should be used for the column. The value is added as a {@link String}.
402         * A column value from the back references takes precedence over a value specified in
403         * {@link #withValues}.
404         * This can only be used with builders of type insert or update.
405         * @return this builder, to allow for chaining.
406         */
407        public Builder withValueBackReferences(ContentValues backReferences) {
408            if (mType != TYPE_INSERT && mType != TYPE_UPDATE) {
409                throw new IllegalArgumentException(
410                        "only inserts and updates can have value back-references");
411            }
412            mValuesBackReferences = backReferences;
413            return this;
414        }
415
416        /**
417         * Add a {@link Map} of back references. The integer key is the index of the selection arg
418         * to set and the integer value is the index of the previous result whose
419         * value should be used for the arg. If any value at that index of the selection arg
420         * that was specified by {@link #withSelection} will be overwritten.
421         * This can only be used with builders of type update, delete, or count query.
422         * @return this builder, to allow for chaining.
423         */
424        public Builder withSelectionBackReferences(Map<Integer, Integer> backReferences) {
425            if (mType != TYPE_COUNT && mType != TYPE_UPDATE && mType != TYPE_DELETE) {
426                throw new IllegalArgumentException(
427                        "only deletes, updates and counts can have selection back-references");
428            }
429            mSelectionArgsBackReferences = backReferences;
430            return this;
431        }
432
433        /**
434         * The ContentValues to use. This may be null. These values may be overwritten by
435         * the corresponding value specified by {@link #withValueBackReferences(ContentValues)}.
436         * This can only be used with builders of type insert or update.
437         * @return this builder, to allow for chaining.
438         */
439        public Builder withValues(ContentValues values) {
440            if (mType != TYPE_INSERT && mType != TYPE_UPDATE) {
441                throw new IllegalArgumentException("only inserts and updates can have values");
442            }
443            mValues = values;
444            return this;
445        }
446
447        /**
448         * The ContentValues to use. This may be null. These values may be overwritten by
449         * the corresponding value specified by {@link #withValueBackReferences(ContentValues)}.
450         * This can only be used with builders of type insert or update.
451         * @return this builder, to allow for chaining.
452         */
453        public Builder withEntity(Entity entity) {
454            if (mType != TYPE_INSERT && mType != TYPE_UPDATE) {
455                throw new IllegalArgumentException("only inserts and updates can have an entity");
456            }
457            mEntity = entity;
458            return this;
459        }
460
461        /**
462         * The selection and arguments to use. An occurrence of '?' in the selection will be
463         * replaced with the corresponding occurence of the selection argument. Any of the
464         * selection arguments may be overwritten by a selection argument back reference as
465         * specified by {@link #withSelectionBackReferences}.
466         * This can only be used with builders of type update, delete, or count query.
467         * @return this builder, to allow for chaining.
468         */
469        public Builder withSelection(String selection, String[] selectionArgs) {
470            if (mType != TYPE_DELETE && mType != TYPE_UPDATE && mType != TYPE_COUNT) {
471                throw new IllegalArgumentException(
472                        "only deletes, updates and counts can have selections");
473            }
474            mSelection = selection;
475            mSelectionArgs = selectionArgs;
476            return this;
477        }
478
479        /**
480         * If set then if the number of rows affected by this operation do not match
481         * this count {@link OperationApplicationException} will be throw.
482         * This can only be used with builders of type update, delete, or count query.
483         * @return this builder, to allow for chaining.
484         */
485        public Builder withExpectedCount(int count) {
486            if (mType != TYPE_DELETE && mType != TYPE_UPDATE && mType != TYPE_COUNT) {
487                throw new IllegalArgumentException(
488                        "only deletes, updates and counts can have expected counts");
489            }
490            mExpectedCount = count;
491            return this;
492        }
493    }
494}
495