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