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      * <p>
    276      * This is intended to be a private method but it is exposed for
    277      * unit testing purposes
    278      * @param backRefs an array of previous results
    279      * @param numBackRefs the number of valid previous results in backRefs
    280      * @return the ContentValues that should be used in this operation application after
    281      * expansion of back references. This can be called if either mValues or mValuesBackReferences
    282      * is null
    283      */
    284     public ContentValues resolveValueBackReferences(
    285             ContentProviderResult[] backRefs, int numBackRefs) {
    286         if (mValuesBackReferences == null) {
    287             return mValues;
    288         }
    289         final ContentValues values;
    290         if (mValues == null) {
    291             values = new ContentValues();
    292         } else {
    293             values = new ContentValues(mValues);
    294         }
    295         for (Map.Entry<String, Object> entry : mValuesBackReferences.valueSet()) {
    296             String key = entry.getKey();
    297             Integer backRefIndex = mValuesBackReferences.getAsInteger(key);
    298             if (backRefIndex == null) {
    299                 Log.e(TAG, this.toString());
    300                 throw new IllegalArgumentException("values backref " + key + " is not an integer");
    301             }
    302             values.put(key, backRefToValue(backRefs, numBackRefs, backRefIndex));
    303         }
    304         return values;
    305     }
    306 
    307     /**
    308      * The Selection Arguments back references are represented as a Map of Integer->Integer where
    309      * the key is an index into the selection argument array (see {@link Builder#withSelection})
    310      * and the value is the index of the previous result that should be used for that selection
    311      * argument array slot.
    312      * <p>
    313      * This is intended to be a private method but it is exposed for
    314      * unit testing purposes
    315      * @param backRefs an array of previous results
    316      * @param numBackRefs the number of valid previous results in backRefs
    317      * @return the ContentValues that should be used in this operation application after
    318      * expansion of back references. This can be called if either mValues or mValuesBackReferences
    319      * is null
    320      */
    321     public String[] resolveSelectionArgsBackReferences(
    322             ContentProviderResult[] backRefs, int numBackRefs) {
    323         if (mSelectionArgsBackReferences == null) {
    324             return mSelectionArgs;
    325         }
    326         String[] newArgs = new String[mSelectionArgs.length];
    327         System.arraycopy(mSelectionArgs, 0, newArgs, 0, mSelectionArgs.length);
    328         for (Map.Entry<Integer, Integer> selectionArgBackRef
    329                 : mSelectionArgsBackReferences.entrySet()) {
    330             final Integer selectionArgIndex = selectionArgBackRef.getKey();
    331             final int backRefIndex = selectionArgBackRef.getValue();
    332             newArgs[selectionArgIndex] =
    333                     String.valueOf(backRefToValue(backRefs, numBackRefs, backRefIndex));
    334         }
    335         return newArgs;
    336     }
    337 
    338     @Override
    339     public String toString() {
    340         return "mType: " + mType + ", mUri: " + mUri +
    341                 ", mSelection: " + mSelection +
    342                 ", mExpectedCount: " + mExpectedCount +
    343                 ", mYieldAllowed: " + mYieldAllowed +
    344                 ", mValues: " + mValues +
    345                 ", mValuesBackReferences: " + mValuesBackReferences +
    346                 ", mSelectionArgsBackReferences: " + mSelectionArgsBackReferences;
    347     }
    348 
    349     /**
    350      * Return the string representation of the requested back reference.
    351      * @param backRefs an array of results
    352      * @param numBackRefs the number of items in the backRefs array that are valid
    353      * @param backRefIndex which backRef to be used
    354      * @throws ArrayIndexOutOfBoundsException thrown if the backRefIndex is larger than
    355      * the numBackRefs
    356      * @return the string representation of the requested back reference.
    357      */
    358     private long backRefToValue(ContentProviderResult[] backRefs, int numBackRefs,
    359             Integer backRefIndex) {
    360         if (backRefIndex >= numBackRefs) {
    361             Log.e(TAG, this.toString());
    362             throw new ArrayIndexOutOfBoundsException("asked for back ref " + backRefIndex
    363                     + " but there are only " + numBackRefs + " back refs");
    364         }
    365         ContentProviderResult backRef = backRefs[backRefIndex];
    366         long backRefValue;
    367         if (backRef.uri != null) {
    368             backRefValue = ContentUris.parseId(backRef.uri);
    369         } else {
    370             backRefValue = backRef.count;
    371         }
    372         return backRefValue;
    373     }
    374 
    375     public int describeContents() {
    376         return 0;
    377     }
    378 
    379     public static final Creator<ContentProviderOperation> CREATOR =
    380             new Creator<ContentProviderOperation>() {
    381         public ContentProviderOperation createFromParcel(Parcel source) {
    382             return new ContentProviderOperation(source);
    383         }
    384 
    385         public ContentProviderOperation[] newArray(int size) {
    386             return new ContentProviderOperation[size];
    387         }
    388     };
    389 
    390 
    391     /**
    392      * Used to add parameters to a {@link ContentProviderOperation}. The {@link Builder} is
    393      * first created by calling {@link ContentProviderOperation#newInsert(android.net.Uri)},
    394      * {@link ContentProviderOperation#newUpdate(android.net.Uri)},
    395      * {@link ContentProviderOperation#newDelete(android.net.Uri)} or
    396      * {@link ContentProviderOperation#newAssertQuery(Uri)}. The withXXX methods
    397      * can then be used to add parameters to the builder. See the specific methods to find for
    398      * which {@link Builder} type each is allowed. Call {@link #build} to create the
    399      * {@link ContentProviderOperation} once all the parameters have been supplied.
    400      */
    401     public static class Builder {
    402         private final int mType;
    403         private final Uri mUri;
    404         private String mSelection;
    405         private String[] mSelectionArgs;
    406         private ContentValues mValues;
    407         private Integer mExpectedCount;
    408         private ContentValues mValuesBackReferences;
    409         private Map<Integer, Integer> mSelectionArgsBackReferences;
    410         private boolean mYieldAllowed;
    411 
    412         /** Create a {@link Builder} of a given type. The uri must not be null. */
    413         private Builder(int type, Uri uri) {
    414             if (uri == null) {
    415                 throw new IllegalArgumentException("uri must not be null");
    416             }
    417             mType = type;
    418             mUri = uri;
    419         }
    420 
    421         /** Create a ContentProviderOperation from this {@link Builder}. */
    422         public ContentProviderOperation build() {
    423             if (mType == TYPE_UPDATE) {
    424                 if ((mValues == null || mValues.size() == 0)
    425                         && (mValuesBackReferences == null || mValuesBackReferences.size() == 0)) {
    426                     throw new IllegalArgumentException("Empty values");
    427                 }
    428             }
    429             if (mType == TYPE_ASSERT) {
    430                 if ((mValues == null || mValues.size() == 0)
    431                         && (mValuesBackReferences == null || mValuesBackReferences.size() == 0)
    432                         && (mExpectedCount == null)) {
    433                     throw new IllegalArgumentException("Empty values");
    434                 }
    435             }
    436             return new ContentProviderOperation(this);
    437         }
    438 
    439         /**
    440          * Add a {@link ContentValues} of back references. The key is the name of the column
    441          * and the value is an integer that is the index of the previous result whose
    442          * value should be used for the column. The value is added as a {@link String}.
    443          * A column value from the back references takes precedence over a value specified in
    444          * {@link #withValues}.
    445          * This can only be used with builders of type insert, update, or assert.
    446          * @return this builder, to allow for chaining.
    447          */
    448         public Builder withValueBackReferences(ContentValues backReferences) {
    449             if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) {
    450                 throw new IllegalArgumentException(
    451                         "only inserts, updates, and asserts can have value back-references");
    452             }
    453             mValuesBackReferences = backReferences;
    454             return this;
    455         }
    456 
    457         /**
    458          * Add a ContentValues back reference.
    459          * A column value from the back references takes precedence over a value specified in
    460          * {@link #withValues}.
    461          * This can only be used with builders of type insert, update, or assert.
    462          * @return this builder, to allow for chaining.
    463          */
    464         public Builder withValueBackReference(String key, int previousResult) {
    465             if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) {
    466                 throw new IllegalArgumentException(
    467                         "only inserts, updates, and asserts can have value back-references");
    468             }
    469             if (mValuesBackReferences == null) {
    470                 mValuesBackReferences = new ContentValues();
    471             }
    472             mValuesBackReferences.put(key, previousResult);
    473             return this;
    474         }
    475 
    476         /**
    477          * Add a back references as a selection arg. Any value at that index of the selection arg
    478          * that was specified by {@link #withSelection} will be overwritten.
    479          * This can only be used with builders of type update, delete, or assert.
    480          * @return this builder, to allow for chaining.
    481          */
    482         public Builder withSelectionBackReference(int selectionArgIndex, int previousResult) {
    483             if (mType != TYPE_UPDATE && mType != TYPE_DELETE && mType != TYPE_ASSERT) {
    484                 throw new IllegalArgumentException("only updates, deletes, and asserts "
    485                         + "can have selection back-references");
    486             }
    487             if (mSelectionArgsBackReferences == null) {
    488                 mSelectionArgsBackReferences = new HashMap<Integer, Integer>();
    489             }
    490             mSelectionArgsBackReferences.put(selectionArgIndex, previousResult);
    491             return this;
    492         }
    493 
    494         /**
    495          * The ContentValues to use. This may be null. These values may be overwritten by
    496          * the corresponding value specified by {@link #withValueBackReference} or by
    497          * future calls to {@link #withValues} or {@link #withValue}.
    498          * This can only be used with builders of type insert, update, or assert.
    499          * @return this builder, to allow for chaining.
    500          */
    501         public Builder withValues(ContentValues values) {
    502             if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) {
    503                 throw new IllegalArgumentException(
    504                         "only inserts, updates, and asserts can have values");
    505             }
    506             if (mValues == null) {
    507                 mValues = new ContentValues();
    508             }
    509             mValues.putAll(values);
    510             return this;
    511         }
    512 
    513         /**
    514          * A value to insert or update. This value may be overwritten by
    515          * the corresponding value specified by {@link #withValueBackReference}.
    516          * This can only be used with builders of type insert, update, or assert.
    517          * @param key the name of this value
    518          * @param value the value itself. the type must be acceptable for insertion by
    519          * {@link ContentValues#put}
    520          * @return this builder, to allow for chaining.
    521          */
    522         public Builder withValue(String key, Object value) {
    523             if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) {
    524                 throw new IllegalArgumentException("only inserts and updates can have values");
    525             }
    526             if (mValues == null) {
    527                 mValues = new ContentValues();
    528             }
    529             if (value == null) {
    530                 mValues.putNull(key);
    531             } else if (value instanceof String) {
    532                 mValues.put(key, (String) value);
    533             } else if (value instanceof Byte) {
    534                 mValues.put(key, (Byte) value);
    535             } else if (value instanceof Short) {
    536                 mValues.put(key, (Short) value);
    537             } else if (value instanceof Integer) {
    538                 mValues.put(key, (Integer) value);
    539             } else if (value instanceof Long) {
    540                 mValues.put(key, (Long) value);
    541             } else if (value instanceof Float) {
    542                 mValues.put(key, (Float) value);
    543             } else if (value instanceof Double) {
    544                 mValues.put(key, (Double) value);
    545             } else if (value instanceof Boolean) {
    546                 mValues.put(key, (Boolean) value);
    547             } else if (value instanceof byte[]) {
    548                 mValues.put(key, (byte[]) value);
    549             } else {
    550                 throw new IllegalArgumentException("bad value type: " + value.getClass().getName());
    551             }
    552             return this;
    553         }
    554 
    555         /**
    556          * The selection and arguments to use. An occurrence of '?' in the selection will be
    557          * replaced with the corresponding occurence of the selection argument. Any of the
    558          * selection arguments may be overwritten by a selection argument back reference as
    559          * specified by {@link #withSelectionBackReference}.
    560          * This can only be used with builders of type update, delete, or assert.
    561          * @return this builder, to allow for chaining.
    562          */
    563         public Builder withSelection(String selection, String[] selectionArgs) {
    564             if (mType != TYPE_UPDATE && mType != TYPE_DELETE && mType != TYPE_ASSERT) {
    565                 throw new IllegalArgumentException(
    566                         "only updates, deletes, and asserts can have selections");
    567             }
    568             mSelection = selection;
    569             if (selectionArgs == null) {
    570                 mSelectionArgs = null;
    571             } else {
    572                 mSelectionArgs = new String[selectionArgs.length];
    573                 System.arraycopy(selectionArgs, 0, mSelectionArgs, 0, selectionArgs.length);
    574             }
    575             return this;
    576         }
    577 
    578         /**
    579          * If set then if the number of rows affected by this operation do not match
    580          * this count {@link OperationApplicationException} will be throw.
    581          * This can only be used with builders of type update, delete, or assert.
    582          * @return this builder, to allow for chaining.
    583          */
    584         public Builder withExpectedCount(int count) {
    585             if (mType != TYPE_UPDATE && mType != TYPE_DELETE && mType != TYPE_ASSERT) {
    586                 throw new IllegalArgumentException(
    587                         "only updates, deletes, and asserts can have expected counts");
    588             }
    589             mExpectedCount = count;
    590             return this;
    591         }
    592 
    593         public Builder withYieldAllowed(boolean yieldAllowed) {
    594             mYieldAllowed = yieldAllowed;
    595             return this;
    596         }
    597     }
    598 }
    599