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