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