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