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