New ContentProviderOperation to assert values during batch.

When performing a set of batch operations, some callers need
to enforce that a query has specific values.  For example,
when persisting edited Contact values, we need to assert
that the RawContacts.VERSION matches the version we read out
through queryEntities().

This change adds a new TYPE_ASSERT that uses withValues()
and withSelection(), and checks all values when applying the
batch operation, bailing if any values don't match.
This commit is contained in:
Jeff Sharkey
2009-08-01 20:13:45 -07:00
parent 3afaaf7ade
commit 08b75b1ffb
3 changed files with 110 additions and 71 deletions

View File

@@ -29268,7 +29268,7 @@
visibility="public"
>
</method>
<method name="newCountQuery"
<method name="newAssertQuery"
return="android.content.ContentProviderOperation.Builder"
abstract="false"
native="false"

View File

@@ -16,14 +16,15 @@
package android.content;
import android.net.Uri;
import android.database.Cursor;
import android.os.Parcelable;
import android.net.Uri;
import android.os.Parcel;
import android.os.Debug;
import android.os.Parcelable;
import android.text.TextUtils;
import java.util.Map;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
public class ContentProviderOperation implements Parcelable {
/** @hide exposed for unit tests */
@@ -33,7 +34,7 @@ public class ContentProviderOperation implements Parcelable {
/** @hide exposed for unit tests */
public final static int TYPE_DELETE = 3;
/** @hide exposed for unit tests */
public final static int TYPE_COUNT = 4;
public final static int TYPE_ASSERT = 4;
private final int mType;
private final Uri mUri;
@@ -44,8 +45,6 @@ public class ContentProviderOperation implements Parcelable {
private final ContentValues mValuesBackReferences;
private final Map<Integer, Integer> mSelectionArgsBackReferences;
private static final String[] COUNT_COLUMNS = new String[]{"count(*)"};
/**
* Creates a {@link ContentProviderOperation} by copying the contents of a
* {@link Builder}.
@@ -156,15 +155,12 @@ public class ContentProviderOperation implements Parcelable {
}
/**
* Create a {@link Builder} suitable for building a count query. When used in conjunction
* with {@link Builder#withExpectedCount(int)} this is useful for checking that the
* uri/selection has the expected number of rows.
* {@link ContentProviderOperation}.
* @param uri The {@link Uri} to query.
* @return a {@link Builder}
* Create a {@link Builder} suitable for building a
* {@link ContentProviderOperation} to assert a set of values as provided
* through {@link Builder#withValues(ContentValues)}.
*/
public static Builder newCountQuery(Uri uri) {
return new Builder(TYPE_COUNT, uri);
public static Builder newAssertQuery(Uri uri) {
return new Builder(TYPE_ASSERT, uri);
}
public Uri getUri() {
@@ -181,7 +177,7 @@ public class ContentProviderOperation implements Parcelable {
}
public boolean isReadOperation() {
return mType == TYPE_COUNT;
return mType == TYPE_ASSERT;
}
/**
@@ -217,18 +213,30 @@ public class ContentProviderOperation implements Parcelable {
numRows = provider.delete(mUri, mSelection, selectionArgs);
} else if (mType == TYPE_UPDATE) {
numRows = provider.update(mUri, values, mSelection, selectionArgs);
} else if (mType == TYPE_COUNT) {
Cursor cursor = provider.query(mUri, COUNT_COLUMNS, mSelection, selectionArgs, null);
} else if (mType == TYPE_ASSERT) {
// Build projection map from expected values
final ArrayList<String> projectionList = new ArrayList<String>();
for (Map.Entry<String, Object> entry : values.valueSet()) {
projectionList.add(entry.getKey());
}
// Assert that all rows match expected values
final String[] projection = projectionList.toArray(new String[projectionList.size()]);
final Cursor cursor = provider.query(mUri, projection, mSelection, selectionArgs, null);
numRows = cursor.getCount();
try {
if (!cursor.moveToNext()) {
throw new RuntimeException("since we are doing a count query we should always "
+ "be able to move to the first row");
while (cursor.moveToNext()) {
for (int i = 0; i < projection.length; i++) {
final String cursorValue = cursor.getString(i);
final String expectedValue = values.getAsString(projection[i]);
if (!TextUtils.equals(cursorValue, expectedValue)) {
// Throw exception when expected values don't match
throw new OperationApplicationException("Found value " + cursorValue
+ " when expected " + expectedValue + " for column "
+ projection[i]);
}
}
}
if (cursor.getCount() != 1) {
throw new RuntimeException("since we are doing a count query there should "
+ "always be exacly row, found " + cursor.getCount());
}
numRows = cursor.getInt(0);
} finally {
cursor.close();
}
@@ -353,7 +361,7 @@ public class ContentProviderOperation implements Parcelable {
* first created by calling {@link ContentProviderOperation#newInsert(android.net.Uri)},
* {@link ContentProviderOperation#newUpdate(android.net.Uri)},
* {@link ContentProviderOperation#newDelete(android.net.Uri)} or
* {@link ContentProviderOperation#newCountQuery(android.net.Uri)}. The withXXX methods
* {@link ContentProviderOperation#newAssertQuery(Uri)}. The withXXX methods
* can then be used to add parameters to the builder. See the specific methods to find for
* which {@link Builder} type each is allowed. Call {@link #build} to create the
* {@link ContentProviderOperation} once all the parameters have been supplied.
@@ -379,7 +387,7 @@ public class ContentProviderOperation implements Parcelable {
/** Create a ContentProviderOperation from this {@link Builder}. */
public ContentProviderOperation build() {
if (mType == TYPE_UPDATE) {
if (mType == TYPE_UPDATE || mType == TYPE_ASSERT) {
if ((mValues == null || mValues.size() == 0)
&& (mValuesBackReferences == null || mValuesBackReferences.size() == 0)) {
throw new IllegalArgumentException("Empty values");
@@ -394,13 +402,13 @@ public class ContentProviderOperation implements Parcelable {
* value should be used for the column. The value is added as a {@link String}.
* A column value from the back references takes precedence over a value specified in
* {@link #withValues}.
* This can only be used with builders of type insert or update.
* This can only be used with builders of type insert, update, or assert.
* @return this builder, to allow for chaining.
*/
public Builder withValueBackReferences(ContentValues backReferences) {
if (mType != TYPE_INSERT && mType != TYPE_UPDATE) {
if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) {
throw new IllegalArgumentException(
"only inserts and updates can have value back-references");
"only inserts, updates, and asserts can have value back-references");
}
mValuesBackReferences = backReferences;
return this;
@@ -410,13 +418,13 @@ public class ContentProviderOperation implements Parcelable {
* Add a ContentValues back reference.
* A column value from the back references takes precedence over a value specified in
* {@link #withValues}.
* This can only be used with builders of type insert or update.
* This can only be used with builders of type insert, update, or assert.
* @return this builder, to allow for chaining.
*/
public Builder withValueBackReference(String key, int previousResult) {
if (mType != TYPE_INSERT && mType != TYPE_UPDATE) {
if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) {
throw new IllegalArgumentException(
"only inserts and updates can have value back-references");
"only inserts, updates, and asserts can have value back-references");
}
if (mValuesBackReferences == null) {
mValuesBackReferences = new ContentValues();
@@ -428,13 +436,13 @@ public class ContentProviderOperation implements Parcelable {
/**
* Add a back references as a selection arg. Any value at that index of the selection arg
* that was specified by {@link #withSelection} will be overwritten.
* This can only be used with builders of type update, delete, or count query.
* This can only be used with builders of type update, delete, or assert.
* @return this builder, to allow for chaining.
*/
public Builder withSelectionBackReference(int selectionArgIndex, int previousResult) {
if (mType != TYPE_COUNT && mType != TYPE_UPDATE && mType != TYPE_DELETE) {
throw new IllegalArgumentException(
"only deletes, updates and counts can have selection back-references");
if (mType != TYPE_UPDATE && mType != TYPE_DELETE && mType != TYPE_ASSERT) {
throw new IllegalArgumentException("only updates, deletes, and asserts "
+ "can have selection back-references");
}
if (mSelectionArgsBackReferences == null) {
mSelectionArgsBackReferences = new HashMap<Integer, Integer>();
@@ -447,12 +455,13 @@ public class ContentProviderOperation implements Parcelable {
* The ContentValues to use. This may be null. These values may be overwritten by
* the corresponding value specified by {@link #withValueBackReference} or by
* future calls to {@link #withValues} or {@link #withValue}.
* This can only be used with builders of type insert or update.
* This can only be used with builders of type insert, update, or assert.
* @return this builder, to allow for chaining.
*/
public Builder withValues(ContentValues values) {
if (mType != TYPE_INSERT && mType != TYPE_UPDATE) {
throw new IllegalArgumentException("only inserts and updates can have values");
if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) {
throw new IllegalArgumentException(
"only inserts, updates, and asserts can have values");
}
if (mValues == null) {
mValues = new ContentValues();
@@ -464,14 +473,14 @@ public class ContentProviderOperation implements Parcelable {
/**
* A value to insert or update. This value may be overwritten by
* the corresponding value specified by {@link #withValueBackReference}.
* This can only be used with builders of type insert or update.
* This can only be used with builders of type insert, update, or assert.
* @param key the name of this value
* @param value the value itself. the type must be acceptable for insertion by
* {@link ContentValues#put}
* @return this builder, to allow for chaining.
*/
public Builder withValue(String key, Object value) {
if (mType != TYPE_INSERT && mType != TYPE_UPDATE) {
if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) {
throw new IllegalArgumentException("only inserts and updates can have values");
}
if (mValues == null) {
@@ -508,13 +517,13 @@ public class ContentProviderOperation implements Parcelable {
* replaced with the corresponding occurence of the selection argument. Any of the
* selection arguments may be overwritten by a selection argument back reference as
* specified by {@link #withSelectionBackReference}.
* This can only be used with builders of type update, delete, or count query.
* This can only be used with builders of type update, delete, or assert.
* @return this builder, to allow for chaining.
*/
public Builder withSelection(String selection, String[] selectionArgs) {
if (mType != TYPE_DELETE && mType != TYPE_UPDATE && mType != TYPE_COUNT) {
if (mType != TYPE_UPDATE && mType != TYPE_DELETE && mType != TYPE_ASSERT) {
throw new IllegalArgumentException(
"only deletes, updates and counts can have selections");
"only updates, deletes, and asserts can have selections");
}
mSelection = selection;
mSelectionArgs = selectionArgs;
@@ -524,13 +533,13 @@ public class ContentProviderOperation implements Parcelable {
/**
* If set then if the number of rows affected by this operation do not match
* this count {@link OperationApplicationException} will be throw.
* This can only be used with builders of type update, delete, or count query.
* This can only be used with builders of type update, delete, or assert.
* @return this builder, to allow for chaining.
*/
public Builder withExpectedCount(int count) {
if (mType != TYPE_DELETE && mType != TYPE_UPDATE && mType != TYPE_COUNT) {
if (mType != TYPE_UPDATE && mType != TYPE_DELETE && mType != TYPE_ASSERT) {
throw new IllegalArgumentException(
"only deletes, updates and counts can have expected counts");
"only updates, deletes, and asserts can have expected counts");
}
mExpectedCount = count;
return this;

View File

@@ -16,7 +16,9 @@
package android.content;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Parcel;
import android.test.suitebuilder.annotation.SmallTest;
@@ -28,7 +30,9 @@ import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Set;
import java.util.Map;
import java.util.Map.Entry;
@SmallTest
public class ContentProviderOperationTest extends TestCase {
@@ -130,6 +134,46 @@ public class ContentProviderOperationTest extends TestCase {
assertEquals(sTestUri1.buildUpon().appendPath("19").toString(), result.uri.toString());
}
public void testAssert() {
// Build an operation to assert values match provider
ContentProviderOperation op1 = ContentProviderOperation.newAssertQuery(sTestUri1)
.withValues(sTestValues1).build();
try {
// Assert that values match from cursor
ContentProviderResult result = op1.apply(new TestContentProvider() {
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// Return cursor over specific set of values
return getCursor(sTestValues1);
}
}, null, 0);
} catch (OperationApplicationException e) {
fail("newAssert() failed");
}
}
/**
* Build a {@link Cursor} with a single row that contains all values
* provided through the given {@link ContentValues}.
*/
private Cursor getCursor(ContentValues contentValues) {
final Set<Entry<String, Object>> valueSet = contentValues.valueSet();
final String[] keys = new String[valueSet.size()];
final Object[] values = new Object[valueSet.size()];
int i = 0;
for (Entry<String, Object> entry : valueSet) {
keys[i] = entry.getKey();
values[i] = entry.getValue();
i++;
}
final MatrixCursor cursor = new MatrixCursor(keys);
cursor.addRow(values);
return cursor;
}
public void testValueBackRefs() {
ContentValues values = new ContentValues();
values.put("a", "in1");
@@ -167,11 +211,15 @@ public class ContentProviderOperationTest extends TestCase {
String[] selectionArgs = new String[]{"a", null, null, "b", null};
final ContentValues values = new ContentValues();
values.put("unused", "unused");
ContentProviderOperation op1 = ContentProviderOperation.newUpdate(sTestUri1)
.withSelectionBackReference(1, 3)
.withSelectionBackReference(2, 1)
.withSelectionBackReference(4, 2)
.withSelection("unused", selectionArgs)
.withValues(values)
.build();
String[] s2 = op1.resolveSelectionArgsBackReferences(
previousResults, previousResults.length);
@@ -212,7 +260,7 @@ public class ContentProviderOperationTest extends TestCase {
parcel.setDataPosition(0);
op2 = ContentProviderOperation.CREATOR.createFromParcel(parcel);
assertEquals(1 /* ContentProviderOperation.TYPE_INSERT */, operationGetType(op2));
assertEquals(ContentProviderOperation.TYPE_INSERT, operationGetType(op2));
assertEquals("content://goo/bar", operationGetUri(op2).toString());
assertEquals(Integer.valueOf(42), operationGetExpectedCount(op2));
assertEquals("selection", operationGetSelection(op2));
@@ -238,9 +286,8 @@ public class ContentProviderOperationTest extends TestCase {
op1.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
op2 = ContentProviderOperation.CREATOR.createFromParcel(parcel);
assertEquals(2 /* ContentProviderOperation.TYPE_UPDATE */, operationGetType(op2));
assertEquals(ContentProviderOperation.TYPE_UPDATE, operationGetType(op2));
assertEquals("content://goo/bar", operationGetUri(op2).toString());
assertNull(operationGetEntity(op2));
assertNull(operationGetExpectedCount(op2));
assertNull(operationGetSelection(op2));
assertNull(operationGetSelectionArgs(op2));
@@ -261,9 +308,8 @@ public class ContentProviderOperationTest extends TestCase {
op1.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
op2 = ContentProviderOperation.CREATOR.createFromParcel(parcel);
assertEquals(3 /* ContentProviderOperation.TYPE_DELETE */, operationGetType(op2));
assertEquals(ContentProviderOperation.TYPE_DELETE, operationGetType(op2));
assertEquals("content://goo/bar", operationGetUri(op2).toString());
assertNull(operationGetEntity(op2));
assertNull(operationGetExpectedCount(op2));
assertNull(operationGetSelection(op2));
assertNull(operationGetSelectionArgs(op2));
@@ -329,15 +375,6 @@ public class ContentProviderOperationTest extends TestCase {
field.set(builder, values);
}
private void builderSetEntity(
ContentProviderOperation.Builder builder, Entity entity)
throws NoSuchFieldException, IllegalAccessException {
Field field;
field = CLASS_BUILDER.getDeclaredField("mEntity");
field.setAccessible(true);
field.set(builder, entity);
}
private void builderSetExpectedCount(
ContentProviderOperation.Builder builder, Integer expectedCount)
throws NoSuchFieldException, IllegalAccessException {
@@ -382,13 +419,6 @@ public class ContentProviderOperationTest extends TestCase {
return (ContentValues) field.get(operation);
}
private Entity operationGetEntity(ContentProviderOperation operation)
throws NoSuchFieldException, IllegalAccessException {
final Field field = CLASS_OPERATION.getDeclaredField("mEntity");
field.setAccessible(true);
return (Entity) field.get(operation);
}
private Integer operationGetExpectedCount(ContentProviderOperation operation)
throws NoSuchFieldException, IllegalAccessException {
final Field field = CLASS_OPERATION.getDeclaredField("mExpectedCount");