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