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