ContentProviderOperation.java revision 5ab78057a35dc71b2847920031cd707a7e2c6c64
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.database.Cursor;
20import android.net.Uri;
21import android.os.Parcel;
22import android.os.Parcelable;
23import android.text.TextUtils;
24
25import java.util.ArrayList;
26import java.util.HashMap;
27import java.util.Map;
28
29public class ContentProviderOperation implements Parcelable {
30    /** @hide exposed for unit tests */
31    public final static int TYPE_INSERT = 1;
32    /** @hide exposed for unit tests */
33    public final static int TYPE_UPDATE = 2;
34    /** @hide exposed for unit tests */
35    public final static int TYPE_DELETE = 3;
36    /** @hide exposed for unit tests */
37    public final static int TYPE_ASSERT = 4;
38
39    private final int mType;
40    private final Uri mUri;
41    private final String mSelection;
42    private final String[] mSelectionArgs;
43    private final ContentValues mValues;
44    private final Integer mExpectedCount;
45    private final ContentValues mValuesBackReferences;
46    private final Map<Integer, Integer> mSelectionArgsBackReferences;
47    private final boolean mYieldAllowed;
48
49    /**
50     * Creates a {@link ContentProviderOperation} by copying the contents of a
51     * {@link Builder}.
52     */
53    private ContentProviderOperation(Builder builder) {
54        mType = builder.mType;
55        mUri = builder.mUri;
56        mValues = builder.mValues;
57        mSelection = builder.mSelection;
58        mSelectionArgs = builder.mSelectionArgs;
59        mExpectedCount = builder.mExpectedCount;
60        mSelectionArgsBackReferences = builder.mSelectionArgsBackReferences;
61        mValuesBackReferences = builder.mValuesBackReferences;
62        mYieldAllowed = builder.mYieldAllowed;
63    }
64
65    private ContentProviderOperation(Parcel source) {
66        mType = source.readInt();
67        mUri = Uri.CREATOR.createFromParcel(source);
68        mValues = source.readInt() != 0 ? ContentValues.CREATOR.createFromParcel(source) : null;
69        mSelection = source.readInt() != 0 ? source.readString() : null;
70        mSelectionArgs = source.readInt() != 0 ? source.readStringArray() : null;
71        mExpectedCount = source.readInt() != 0 ? source.readInt() : null;
72        mValuesBackReferences = source.readInt() != 0
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        mYieldAllowed = source.readInt() != 0;
85    }
86
87    public void writeToParcel(Parcel dest, int flags) {
88        dest.writeInt(mType);
89        Uri.writeToParcel(dest, mUri);
90        if (mValues != null) {
91            dest.writeInt(1);
92            mValues.writeToParcel(dest, 0);
93        } else {
94            dest.writeInt(0);
95        }
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        dest.writeInt(mYieldAllowed ? 1 : 0);
131    }
132
133    /**
134     * Create a {@link Builder} suitable for building an insert {@link ContentProviderOperation}.
135     * @param uri The {@link Uri} that is the target of the insert.
136     * @return a {@link Builder}
137     */
138    public static Builder newInsert(Uri uri) {
139        return new Builder(TYPE_INSERT, uri);
140    }
141
142    /**
143     * Create a {@link Builder} suitable for building an update {@link ContentProviderOperation}.
144     * @param uri The {@link Uri} that is the target of the update.
145     * @return a {@link Builder}
146     */
147    public static Builder newUpdate(Uri uri) {
148        return new Builder(TYPE_UPDATE, uri);
149    }
150
151    /**
152     * Create a {@link Builder} suitable for building a delete {@link ContentProviderOperation}.
153     * @param uri The {@link Uri} that is the target of the delete.
154     * @return a {@link Builder}
155     */
156    public static Builder newDelete(Uri uri) {
157        return new Builder(TYPE_DELETE, uri);
158    }
159
160    /**
161     * Create a {@link Builder} suitable for building a
162     * {@link ContentProviderOperation} to assert a set of values as provided
163     * through {@link Builder#withValues(ContentValues)}.
164     */
165    public static Builder newAssertQuery(Uri uri) {
166        return new Builder(TYPE_ASSERT, uri);
167    }
168
169    public Uri getUri() {
170        return mUri;
171    }
172
173    public boolean isYieldAllowed() {
174        return mYieldAllowed;
175    }
176
177    /** @hide exposed for unit tests */
178    public int getType() {
179        return mType;
180    }
181
182    public boolean isWriteOperation() {
183        return mType == TYPE_DELETE || mType == TYPE_INSERT || mType == TYPE_UPDATE;
184    }
185
186    public boolean isReadOperation() {
187        return mType == TYPE_ASSERT;
188    }
189
190    /**
191     * Applies this operation using the given provider. The backRefs array is used to resolve any
192     * back references that were requested using
193     * {@link Builder#withValueBackReferences(ContentValues)} and
194     * {@link Builder#withSelectionBackReference}.
195     * @param provider the {@link ContentProvider} on which this batch is applied
196     * @param backRefs a {@link ContentProviderResult} array that will be consulted
197     * to resolve any requested back references.
198     * @param numBackRefs the number of valid results on the backRefs array.
199     * @return a {@link ContentProviderResult} that contains either the {@link Uri} of the inserted
200     * row if this was an insert otherwise the number of rows affected.
201     * @throws OperationApplicationException thrown if either the insert fails or
202     * if the number of rows affected didn't match the expected count
203     */
204    public ContentProviderResult apply(ContentProvider provider, ContentProviderResult[] backRefs,
205            int numBackRefs) throws OperationApplicationException {
206        ContentValues values = resolveValueBackReferences(backRefs, numBackRefs);
207        String[] selectionArgs =
208                resolveSelectionArgsBackReferences(backRefs, numBackRefs);
209
210        if (mType == TYPE_INSERT) {
211            Uri newUri = provider.insert(mUri, values);
212            if (newUri == null) {
213                throw new OperationApplicationException("insert failed");
214            }
215            return new ContentProviderResult(newUri);
216        }
217
218        int numRows;
219        if (mType == TYPE_DELETE) {
220            numRows = provider.delete(mUri, mSelection, selectionArgs);
221        } else if (mType == TYPE_UPDATE) {
222            numRows = provider.update(mUri, values, mSelection, selectionArgs);
223        } else if (mType == TYPE_ASSERT) {
224            // Assert that all rows match expected values
225            String[] projection =  null;
226            if (values != null) {
227                // Build projection map from expected values
228                final ArrayList<String> projectionList = new ArrayList<String>();
229                for (Map.Entry<String, Object> entry : values.valueSet()) {
230                    projectionList.add(entry.getKey());
231                }
232                projection = projectionList.toArray(new String[projectionList.size()]);
233            }
234            final Cursor cursor = provider.query(mUri, projection, mSelection, selectionArgs, null);
235            try {
236                numRows = cursor.getCount();
237                if (projection != null) {
238                    while (cursor.moveToNext()) {
239                        for (int i = 0; i < projection.length; i++) {
240                            final String cursorValue = cursor.getString(i);
241                            final String expectedValue = values.getAsString(projection[i]);
242                            if (!TextUtils.equals(cursorValue, expectedValue)) {
243                                // Throw exception when expected values don't match
244                                throw new OperationApplicationException("Found value " + cursorValue
245                                        + " when expected " + expectedValue + " for column "
246                                        + projection[i]);
247                            }
248                        }
249                    }
250                }
251            } finally {
252                cursor.close();
253            }
254        } else {
255            throw new IllegalStateException("bad type, " + mType);
256        }
257
258        if (mExpectedCount != null && mExpectedCount != numRows) {
259            throw new OperationApplicationException("wrong number of rows: " + numRows);
260        }
261
262        return new ContentProviderResult(numRows);
263    }
264
265    /**
266     * The ContentValues back references are represented as a ContentValues object where the
267     * key refers to a column and the value is an index of the back reference whose
268     * valued should be associated with the column.
269     * @param backRefs an array of previous results
270     * @param numBackRefs the number of valid previous results in backRefs
271     * @return the ContentValues that should be used in this operation application after
272     * expansion of back references. This can be called if either mValues or mValuesBackReferences
273     * is null
274     * @VisibleForTesting this is intended to be a private method but it is exposed for
275     * unit testing purposes
276     */
277    public ContentValues resolveValueBackReferences(
278            ContentProviderResult[] backRefs, int numBackRefs) {
279        if (mValuesBackReferences == null) {
280            return mValues;
281        }
282        final ContentValues values;
283        if (mValues == null) {
284            values = new ContentValues();
285        } else {
286            values = new ContentValues(mValues);
287        }
288        for (Map.Entry<String, Object> entry : mValuesBackReferences.valueSet()) {
289            String key = entry.getKey();
290            Integer backRefIndex = mValuesBackReferences.getAsInteger(key);
291            if (backRefIndex == null) {
292                throw new IllegalArgumentException("values backref " + key + " is not an integer");
293            }
294            values.put(key, backRefToValue(backRefs, numBackRefs, backRefIndex));
295        }
296        return values;
297    }
298
299    /**
300     * The Selection Arguments back references are represented as a Map of Integer->Integer where
301     * the key is an index into the selection argument array (see {@link Builder#withSelection})
302     * and the value is the index of the previous result that should be used for that selection
303     * argument array slot.
304     * @param backRefs an array of previous results
305     * @param numBackRefs the number of valid previous results in backRefs
306     * @return the ContentValues that should be used in this operation application after
307     * expansion of back references. This can be called if either mValues or mValuesBackReferences
308     * is null
309     * @VisibleForTesting this is intended to be a private method but it is exposed for
310     * unit testing purposes
311     */
312    public String[] resolveSelectionArgsBackReferences(
313            ContentProviderResult[] backRefs, int numBackRefs) {
314        if (mSelectionArgsBackReferences == null) {
315            return mSelectionArgs;
316        }
317        String[] newArgs = new String[mSelectionArgs.length];
318        System.arraycopy(mSelectionArgs, 0, newArgs, 0, mSelectionArgs.length);
319        for (Map.Entry<Integer, Integer> selectionArgBackRef
320                : mSelectionArgsBackReferences.entrySet()) {
321            final Integer selectionArgIndex = selectionArgBackRef.getKey();
322            final int backRefIndex = selectionArgBackRef.getValue();
323            newArgs[selectionArgIndex] =
324                    String.valueOf(backRefToValue(backRefs, numBackRefs, backRefIndex));
325        }
326        return newArgs;
327    }
328
329    /**
330     * Return the string representation of the requested back reference.
331     * @param backRefs an array of results
332     * @param numBackRefs the number of items in the backRefs array that are valid
333     * @param backRefIndex which backRef to be used
334     * @throws ArrayIndexOutOfBoundsException thrown if the backRefIndex is larger than
335     * the numBackRefs
336     * @return the string representation of the requested back reference.
337     */
338    private static long backRefToValue(ContentProviderResult[] backRefs, int numBackRefs,
339            Integer backRefIndex) {
340        if (backRefIndex >= numBackRefs) {
341            throw new ArrayIndexOutOfBoundsException("asked for back ref " + backRefIndex
342                    + " but there are only " + numBackRefs + " back refs");
343        }
344        ContentProviderResult backRef = backRefs[backRefIndex];
345        long backRefValue;
346        if (backRef.uri != null) {
347            backRefValue = ContentUris.parseId(backRef.uri);
348        } else {
349            backRefValue = backRef.count;
350        }
351        return backRefValue;
352    }
353
354    public int describeContents() {
355        return 0;
356    }
357
358    public static final Creator<ContentProviderOperation> CREATOR =
359            new Creator<ContentProviderOperation>() {
360        public ContentProviderOperation createFromParcel(Parcel source) {
361            return new ContentProviderOperation(source);
362        }
363
364        public ContentProviderOperation[] newArray(int size) {
365            return new ContentProviderOperation[size];
366        }
367    };
368
369
370    /**
371     * Used to add parameters to a {@link ContentProviderOperation}. The {@link Builder} is
372     * first created by calling {@link ContentProviderOperation#newInsert(android.net.Uri)},
373     * {@link ContentProviderOperation#newUpdate(android.net.Uri)},
374     * {@link ContentProviderOperation#newDelete(android.net.Uri)} or
375     * {@link ContentProviderOperation#newAssertQuery(Uri)}. The withXXX methods
376     * can then be used to add parameters to the builder. See the specific methods to find for
377     * which {@link Builder} type each is allowed. Call {@link #build} to create the
378     * {@link ContentProviderOperation} once all the parameters have been supplied.
379     */
380    public static class Builder {
381        private final int mType;
382        private final Uri mUri;
383        private String mSelection;
384        private String[] mSelectionArgs;
385        private ContentValues mValues;
386        private Integer mExpectedCount;
387        private ContentValues mValuesBackReferences;
388        private Map<Integer, Integer> mSelectionArgsBackReferences;
389        private boolean mYieldAllowed;
390
391        /** Create a {@link Builder} of a given type. The uri must not be null. */
392        private Builder(int type, Uri uri) {
393            if (uri == null) {
394                throw new IllegalArgumentException("uri must not be null");
395            }
396            mType = type;
397            mUri = uri;
398        }
399
400        /** Create a ContentProviderOperation from this {@link Builder}. */
401        public ContentProviderOperation build() {
402            if (mType == TYPE_UPDATE) {
403                if ((mValues == null || mValues.size() == 0)
404                        && (mValuesBackReferences == null || mValuesBackReferences.size() == 0)) {
405                    throw new IllegalArgumentException("Empty values");
406                }
407            }
408            if (mType == TYPE_ASSERT) {
409                if ((mValues == null || mValues.size() == 0)
410                        && (mValuesBackReferences == null || mValuesBackReferences.size() == 0)
411                        && (mExpectedCount == null)) {
412                    throw new IllegalArgumentException("Empty values");
413                }
414            }
415            return new ContentProviderOperation(this);
416        }
417
418        /**
419         * Add a {@link ContentValues} of back references. The key is the name of the column
420         * and the value is an integer that is the index of the previous result whose
421         * value should be used for the column. The value is added as a {@link String}.
422         * A column value from the back references takes precedence over a value specified in
423         * {@link #withValues}.
424         * This can only be used with builders of type insert, update, or assert.
425         * @return this builder, to allow for chaining.
426         */
427        public Builder withValueBackReferences(ContentValues backReferences) {
428            if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) {
429                throw new IllegalArgumentException(
430                        "only inserts, updates, and asserts can have value back-references");
431            }
432            mValuesBackReferences = backReferences;
433            return this;
434        }
435
436        /**
437         * Add a ContentValues back reference.
438         * A column value from the back references takes precedence over a value specified in
439         * {@link #withValues}.
440         * This can only be used with builders of type insert, update, or assert.
441         * @return this builder, to allow for chaining.
442         */
443        public Builder withValueBackReference(String key, int previousResult) {
444            if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) {
445                throw new IllegalArgumentException(
446                        "only inserts, updates, and asserts can have value back-references");
447            }
448            if (mValuesBackReferences == null) {
449                mValuesBackReferences = new ContentValues();
450            }
451            mValuesBackReferences.put(key, previousResult);
452            return this;
453        }
454
455        /**
456         * Add a back references as a selection arg. Any value at that index of the selection arg
457         * that was specified by {@link #withSelection} will be overwritten.
458         * This can only be used with builders of type update, delete, or assert.
459         * @return this builder, to allow for chaining.
460         */
461        public Builder withSelectionBackReference(int selectionArgIndex, int previousResult) {
462            if (mType != TYPE_UPDATE && mType != TYPE_DELETE && mType != TYPE_ASSERT) {
463                throw new IllegalArgumentException("only updates, deletes, and asserts "
464                        + "can have selection back-references");
465            }
466            if (mSelectionArgsBackReferences == null) {
467                mSelectionArgsBackReferences = new HashMap<Integer, Integer>();
468            }
469            mSelectionArgsBackReferences.put(selectionArgIndex, previousResult);
470            return this;
471        }
472
473        /**
474         * The ContentValues to use. This may be null. These values may be overwritten by
475         * the corresponding value specified by {@link #withValueBackReference} or by
476         * future calls to {@link #withValues} or {@link #withValue}.
477         * This can only be used with builders of type insert, update, or assert.
478         * @return this builder, to allow for chaining.
479         */
480        public Builder withValues(ContentValues values) {
481            if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) {
482                throw new IllegalArgumentException(
483                        "only inserts, updates, and asserts can have values");
484            }
485            if (mValues == null) {
486                mValues = new ContentValues();
487            }
488            mValues.putAll(values);
489            return this;
490        }
491
492        /**
493         * A value to insert or update. This value may be overwritten by
494         * the corresponding value specified by {@link #withValueBackReference}.
495         * This can only be used with builders of type insert, update, or assert.
496         * @param key the name of this value
497         * @param value the value itself. the type must be acceptable for insertion by
498         * {@link ContentValues#put}
499         * @return this builder, to allow for chaining.
500         */
501        public Builder withValue(String key, Object value) {
502            if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) {
503                throw new IllegalArgumentException("only inserts and updates can have values");
504            }
505            if (mValues == null) {
506                mValues = new ContentValues();
507            }
508            if (value == null) {
509                mValues.putNull(key);
510            } else if (value instanceof String) {
511                mValues.put(key, (String) value);
512            } else if (value instanceof Byte) {
513                mValues.put(key, (Byte) value);
514            } else if (value instanceof Short) {
515                mValues.put(key, (Short) value);
516            } else if (value instanceof Integer) {
517                mValues.put(key, (Integer) value);
518            } else if (value instanceof Long) {
519                mValues.put(key, (Long) value);
520            } else if (value instanceof Float) {
521                mValues.put(key, (Float) value);
522            } else if (value instanceof Double) {
523                mValues.put(key, (Double) value);
524            } else if (value instanceof Boolean) {
525                mValues.put(key, (Boolean) value);
526            } else if (value instanceof byte[]) {
527                mValues.put(key, (byte[]) value);
528            } else {
529                throw new IllegalArgumentException("bad value type: " + value.getClass().getName());
530            }
531            return this;
532        }
533
534        /**
535         * The selection and arguments to use. An occurrence of '?' in the selection will be
536         * replaced with the corresponding occurence of the selection argument. Any of the
537         * selection arguments may be overwritten by a selection argument back reference as
538         * specified by {@link #withSelectionBackReference}.
539         * This can only be used with builders of type update, delete, or assert.
540         * @return this builder, to allow for chaining.
541         */
542        public Builder withSelection(String selection, String[] selectionArgs) {
543            if (mType != TYPE_UPDATE && mType != TYPE_DELETE && mType != TYPE_ASSERT) {
544                throw new IllegalArgumentException(
545                        "only updates, deletes, and asserts can have selections");
546            }
547            mSelection = selection;
548            mSelectionArgs = selectionArgs;
549            return this;
550        }
551
552        /**
553         * If set then if the number of rows affected by this operation do not match
554         * this count {@link OperationApplicationException} will be throw.
555         * This can only be used with builders of type update, delete, or assert.
556         * @return this builder, to allow for chaining.
557         */
558        public Builder withExpectedCount(int count) {
559            if (mType != TYPE_UPDATE && mType != TYPE_DELETE && mType != TYPE_ASSERT) {
560                throw new IllegalArgumentException(
561                        "only updates, deletes, and asserts can have expected counts");
562            }
563            mExpectedCount = count;
564            return this;
565        }
566
567        public Builder withYieldAllowed(boolean yieldAllowed) {
568            mYieldAllowed = yieldAllowed;
569            return this;
570        }
571    }
572}
573