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