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