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